feat: sidebar navigation with V5.1/V5.2 separate entries

- Sidebar: 信号/模拟盘 section headers
- Three paper trade entries: 全部持仓, V5.1模拟盘, V5.2模拟盘 (NEW badge)
- Paper page reads strategy from URL query params
- Suspense boundary for useSearchParams
This commit is contained in:
root 2026-03-01 12:25:40 +00:00
parent 778cf8cce1
commit ee90b8dcfa
4 changed files with 272 additions and 16 deletions

224
V52_TASK.md Normal file
View File

@ -0,0 +1,224 @@
# V5.2 Development Task
## Context
You are working on the `dev` branch of the ArbitrageEngine project.
This is a quantitative trading signal system with:
- Backend: Python (FastAPI + PostgreSQL)
- Frontend: Next.js + shadcn/ui + Tailwind
## Database Connection
- Host: 34.85.117.248 (Cloud SQL)
- Port: 5432, DB: arb_engine, User: arb, Password: arb_engine_2026
## What to Build (V5.2)
### 1. Strategy Configuration Framework
Create `backend/strategies/` directory with JSON configs:
**backend/strategies/v51_baseline.json:**
```json
{
"name": "v51_baseline",
"version": "5.1",
"threshold": 75,
"weights": {
"direction": 45,
"crowding": 20,
"environment": 15,
"confirmation": 15,
"auxiliary": 5
},
"accel_bonus": 5,
"tp_sl": {
"sl_multiplier": 2.0,
"tp1_multiplier": 1.5,
"tp2_multiplier": 3.0
},
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium"]
}
```
**backend/strategies/v52_8signals.json:**
```json
{
"name": "v52_8signals",
"version": "5.2",
"threshold": 75,
"weights": {
"direction": 40,
"crowding": 25,
"environment": 15,
"confirmation": 20,
"auxiliary": 5
},
"accel_bonus": 5,
"tp_sl": {
"sl_multiplier": 2.0,
"tp1_multiplier": 1.5,
"tp2_multiplier": 3.0
},
"signals": ["cvd", "p99", "accel", "ls_ratio", "oi", "coinbase_premium", "funding_rate", "liquidation"]
}
```
### 2. Signal Engine Changes (signal_engine.py)
#### 2a. Add FR scoring to evaluate_signal()
After the crowding section, add funding_rate scoring:
```python
# Funding Rate scoring (拥挤层加分)
# Read from market_indicators table
funding_rate = to_float(self.market_indicators.get("funding_rate"))
fr_score = 0
if funding_rate is not None:
fr_abs = abs(funding_rate)
if fr_abs >= 0.001: # extreme ±0.1%
# Extreme: penalize if going WITH the crowd
if (direction == "LONG" and funding_rate > 0.001) or \
(direction == "SHORT" and funding_rate < -0.001):
fr_score = -5
else:
fr_score = 5
elif fr_abs >= 0.0003: # moderate ±0.03%
# Moderate: reward going AGAINST the crowd
if (direction == "LONG" and funding_rate < -0.0003) or \
(direction == "SHORT" and funding_rate > 0.0003):
fr_score = 5
else:
fr_score = 0
```
#### 2b. Add liquidation scoring
```python
# Liquidation scoring (确认层加分)
liq_score = 0
liq_data = self.fetch_recent_liquidations() # new method
if liq_data:
liq_long_usd = liq_data.get("long_usd", 0)
liq_short_usd = liq_data.get("short_usd", 0)
# Thresholds by symbol
thresholds = {"BTCUSDT": 500000, "ETHUSDT": 200000, "XRPUSDT": 100000, "SOLUSDT": 100000}
threshold = thresholds.get(self.symbol, 100000)
total = liq_long_usd + liq_short_usd
if total >= threshold:
if liq_short_usd > 0 and liq_long_usd > 0:
ratio = liq_short_usd / liq_long_usd
elif liq_short_usd > 0:
ratio = float('inf')
else:
ratio = 0
if ratio >= 2.0 and direction == "LONG":
liq_score = 5 # shorts getting liquidated, price going up
elif ratio <= 0.5 and direction == "SHORT":
liq_score = 5 # longs getting liquidated, price going down
```
#### 2c. Add fetch_recent_liquidations method to SymbolState
```python
def fetch_recent_liquidations(self, window_ms=300000):
"""Fetch last 5min liquidation totals from liquidations table"""
now_ms = int(time.time() * 1000)
cutoff = now_ms - window_ms
with get_sync_conn() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT
COALESCE(SUM(CASE WHEN side='SELL' THEN usd_value ELSE 0 END), 0) as long_liq,
COALESCE(SUM(CASE WHEN side='BUY' THEN usd_value ELSE 0 END), 0) as short_liq
FROM liquidations
WHERE symbol=%s AND trade_time >= %s
""", (self.symbol, cutoff))
row = cur.fetchone()
if row:
return {"long_usd": row[0], "short_usd": row[1]}
return None
```
#### 2d. Add funding_rate to fetch_market_indicators
Add "funding_rate" to the indicator types:
```python
for ind_type in ["long_short_ratio", "top_trader_position", "open_interest_hist", "coinbase_premium", "funding_rate"]:
```
And the extraction:
```python
elif ind_type == "funding_rate":
indicators[ind_type] = float(val.get("lastFundingRate", 0))
```
#### 2e. Update total_score calculation
Currently:
```python
total_score = direction_score + accel_bonus + crowding_score + environment_score + confirmation_score + aux_score
```
Change to:
```python
total_score = direction_score + accel_bonus + crowding_score + fr_score + environment_score + confirmation_score + liq_score + aux_score
```
#### 2f. Update factors dict
Add fr_score and liq_score to the factors:
```python
result["factors"] = {
...existing factors...,
"funding_rate": {"score": fr_score, "value": funding_rate},
"liquidation": {"score": liq_score, "long_usd": liq_data.get("long_usd", 0) if liq_data else 0, "short_usd": liq_data.get("short_usd", 0) if liq_data else 0},
}
```
#### 2g. Change threshold from 60 to 75
In evaluate_signal, change:
```python
# OLD
elif total_score >= 60 and not no_direction and not in_cooldown:
result["signal"] = direction
result["tier"] = "light"
# NEW: remove the 60 tier entirely, minimum is 75
```
Also update reverse signal threshold from 60 to 75:
In main() loop:
```python
# OLD
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 60:
# NEW
if existing_dir and eval_dir and existing_dir != eval_dir and result["score"] >= 75:
```
### 3. Strategy field in paper_trades
Add SQL migration at top of init_schema() or in a migration:
```sql
ALTER TABLE paper_trades ADD COLUMN IF NOT EXISTS strategy VARCHAR(32) DEFAULT 'v51_baseline';
```
### 4. AB Test: Both strategies evaluate each cycle
In the main loop, evaluate signal twice (once per strategy config) and potentially open trades for both. Each trade records which strategy triggered it.
### 5. Frontend: Update paper/page.tsx
- Show strategy column in trade history table
- Show FR and liquidation scores in signal details
- Add strategy filter/tab (v51 vs v52)
### 6. API: Add strategy stats endpoint
In main.py, add `/api/paper/stats-by-strategy` that groups stats by strategy field.
## Important Notes
- Keep ALL existing functionality working
- Don't break the existing V5.1 scoring - it should still work as strategy "v51_baseline"
- The FR data is already in market_indicators table (collected every 5min)
- The liquidation data is already in liquidations table
- Test with: `cd frontend && npm run build` to verify no frontend errors
- Test backend: `python3 -c "from signal_engine import *; print('OK')"` to verify imports
- Port for dev testing: API=8100, Frontend=3300
- Total score CAN exceed 100 (that's by design)
## Files to modify:
1. `backend/signal_engine.py` - core scoring changes
2. `backend/main.py` - new API endpoints
3. `backend/db.py` - add strategy column migration
4. `frontend/app/paper/page.tsx` - UI updates
5. NEW: `backend/strategies/v51_baseline.json`
6. NEW: `backend/strategies/v52_8signals.json`
When completely finished, run this command to notify me:
openclaw system event --text "Done: V5.2 core implementation complete - FR+liquidation scoring, threshold 75, strategy configs, AB test framework" --mode now

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { authFetch, useAuth } from "@/lib/auth"; import { authFetch, useAuth } from "@/lib/auth";
import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts"; import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts";
@ -657,9 +658,18 @@ function StatsPanel({ strategy }: { strategy: StrategyFilter }) {
// ─── 主页面 ────────────────────────────────────────────────────── // ─── 主页面 ──────────────────────────────────────────────────────
export default function PaperTradingPage() { function PaperTradingPageInner() {
const { isLoggedIn, loading } = useAuth(); const { isLoggedIn, loading } = useAuth();
const [strategyTab, setStrategyTab] = useState<StrategyFilter>("all"); const searchParams = useSearchParams();
const urlStrategy = searchParams.get("strategy");
const [strategyTab, setStrategyTab] = useState<StrategyFilter>(() => normalizeStrategy(urlStrategy));
// URL参数变化时同步
useEffect(() => {
if (urlStrategy) {
setStrategyTab(normalizeStrategy(urlStrategy));
}
}, [urlStrategy]);
if (loading) return <div className="text-center text-slate-400 py-8">...</div>; if (loading) return <div className="text-center text-slate-400 py-8">...</div>;
@ -711,3 +721,11 @@ export default function PaperTradingPage() {
</div> </div>
); );
} }
export default function PaperTradingPage() {
return (
<Suspense fallback={<div className="text-center text-slate-400 py-8">...</div>}>
<PaperTradingPageInner />
</Suspense>
);
}

View File

@ -7,14 +7,16 @@ import { useAuth } from "@/lib/auth";
import { import {
LayoutDashboard, Info, LayoutDashboard, Info,
Menu, X, Zap, LogIn, UserPlus, Menu, X, Zap, LogIn, UserPlus,
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles, FlaskConical
} from "lucide-react"; } from "lucide-react";
const navItems = [ const navItems = [
{ href: "/", label: "仪表盘", icon: LayoutDashboard }, { href: "/", label: "仪表盘", icon: LayoutDashboard },
{ href: "/trades", label: "成交流", icon: Activity }, { href: "/trades", label: "成交流", icon: Activity },
{ href: "/signals", label: "信号引擎 V5.1", icon: Crosshair }, { href: "/signals", label: "信号引擎", icon: Crosshair, section: "信号" },
{ href: "/paper", label: "模拟盘", icon: LineChart }, { href: "/paper?strategy=all", label: "全部持仓", icon: LineChart, section: "模拟盘" },
{ href: "/paper?strategy=v51_baseline", label: "V5.1 模拟盘", icon: FlaskConical },
{ href: "/paper?strategy=v52_8signals", label: "V5.2 模拟盘", icon: Sparkles, badge: "NEW" },
{ href: "/server", label: "服务器", icon: Monitor }, { href: "/server", label: "服务器", icon: Monitor },
{ href: "/about", label: "说明", icon: Info }, { href: "/about", label: "说明", icon: Info },
]; ];
@ -37,17 +39,29 @@ export default function Sidebar() {
{/* Nav */} {/* Nav */}
<nav className="flex-1 py-4 space-y-1 px-2"> <nav className="flex-1 py-4 space-y-1 px-2">
{navItems.map(({ href, label, icon: Icon }) => { {navItems.map(({ href, label, icon: Icon, section, badge }, idx) => {
const active = pathname === href; const active = pathname === href || (href.includes("?") && pathname === href.split("?")[0] && typeof window !== "undefined" && window.location.search === "?" + href.split("?")[1]);
return ( return (
<Link key={href} href={href} <div key={href}>
{section && (
<div className={`px-3 pt-3 pb-1 text-[10px] font-semibold text-slate-400 uppercase tracking-wider ${idx > 0 ? "mt-2 border-t border-slate-100 pt-4" : ""}`}>
{section}
</div>
)}
<Link href={href}
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors
${active ? "bg-blue-50 text-blue-700 font-medium" : "text-slate-600 hover:bg-slate-100 hover:text-slate-900"} ${active ? "bg-blue-50 text-blue-700 font-medium" : "text-slate-600 hover:bg-slate-100 hover:text-slate-900"}
${collapsed && !mobile ? "justify-center" : ""}`}> ${collapsed && !mobile ? "justify-center" : ""}`}>
<Icon className={`shrink-0 ${active ? "text-blue-600" : "text-slate-400"}`} size={18} /> <Icon className={`shrink-0 ${active ? "text-blue-600" : "text-slate-400"}`} size={18} />
{(!collapsed || mobile) && <span>{label}</span>} {(!collapsed || mobile) && (
<span className="flex items-center gap-1.5">
{label}
{badge && <span className="text-[9px] bg-emerald-500 text-white px-1 py-0.5 rounded font-bold">{badge}</span>}
</span>
)}
</Link> </Link>
</div>
); );
})} })}
{/* 手机端:登录/登出放在菜单里 */} {/* 手机端:登录/登出放在菜单里 */}

0
signal-engine.log Normal file
View File