diff --git a/V52_TASK.md b/V52_TASK.md new file mode 100644 index 0000000..0e79d4a --- /dev/null +++ b/V52_TASK.md @@ -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 diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index b6bfeca..fceecb8 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -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("all"); + const searchParams = useSearchParams(); + const urlStrategy = searchParams.get("strategy"); + const [strategyTab, setStrategyTab] = useState(() => normalizeStrategy(urlStrategy)); + + // URL参数变化时同步 + useEffect(() => { + if (urlStrategy) { + setStrategyTab(normalizeStrategy(urlStrategy)); + } + }, [urlStrategy]); if (loading) return
加载中...
; @@ -711,3 +721,11 @@ export default function PaperTradingPage() { ); } + +export default function PaperTradingPage() { + return ( + 加载中...}> + + + ); +} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 4764b85..33be8e5 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -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 */}