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:
parent
778cf8cce1
commit
ee90b8dcfa
224
V52_TASK.md
Normal file
224
V52_TASK.md
Normal 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
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { authFetch, useAuth } from "@/lib/auth";
|
||||
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 [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>;
|
||||
|
||||
@ -711,3 +721,11 @@ export default function PaperTradingPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaperTradingPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="text-center text-slate-400 py-8">加载中...</div>}>
|
||||
<PaperTradingPageInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,14 +7,16 @@ import { useAuth } from "@/lib/auth";
|
||||
import {
|
||||
LayoutDashboard, Info,
|
||||
Menu, X, Zap, LogIn, UserPlus,
|
||||
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart
|
||||
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles, FlaskConical
|
||||
} from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", label: "仪表盘", icon: LayoutDashboard },
|
||||
{ href: "/trades", label: "成交流", icon: Activity },
|
||||
{ href: "/signals", label: "信号引擎 V5.1", icon: Crosshair },
|
||||
{ href: "/paper", label: "模拟盘", icon: LineChart },
|
||||
{ href: "/signals", label: "信号引擎", icon: Crosshair, section: "信号" },
|
||||
{ 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: "/about", label: "说明", icon: Info },
|
||||
];
|
||||
@ -37,17 +39,29 @@ export default function Sidebar() {
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 py-4 space-y-1 px-2">
|
||||
{navItems.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname === href;
|
||||
{navItems.map(({ href, label, icon: Icon, section, badge }, idx) => {
|
||||
const active = pathname === href || (href.includes("?") && pathname === href.split("?")[0] && typeof window !== "undefined" && window.location.search === "?" + href.split("?")[1]);
|
||||
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)}
|
||||
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"}
|
||||
${collapsed && !mobile ? "justify-center" : ""}`}>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 手机端:登录/登出放在菜单里 */}
|
||||
|
||||
0
signal-engine.log
Normal file
0
signal-engine.log
Normal file
Loading…
Reference in New Issue
Block a user