diff --git a/SEPARATION_TASK.md b/SEPARATION_TASK.md new file mode 100644 index 0000000..303aa14 --- /dev/null +++ b/SEPARATION_TASK.md @@ -0,0 +1,77 @@ +# V5.1/V5.2 Complete Separation Task + +## CRITICAL: Boss is angry. Do this PERFECTLY. + +## Requirements + +### 1. V5.1 Signal Engine Page - RESTORE to pre-V5.2 state +- `/signals` page should be V5.1 ONLY +- Title: "⚡ 信号引擎 V5.1" +- Remove ALL V5.2 comparison cards, V5.2 scores, FR/Liq bars +- Only show the original 5 layers: 方向(45) + 拥挤(20) + 环境(15) + 确认(15) + 辅助(5) = 100 +- Do NOT show FR or Liq score bars on V5.1 page + +### 2. V5.2 Signal Engine Page - NEW independent page +- Create `/signals-v52` page +- Title: "⚡ 信号引擎 V5.2" +- Show 7 layers: 方向(40) + 拥挤(20) + FR(5) + 环境(15) + 确认(15) + 清算(5) + 辅助(5) = 105 max BUT... +- **FIX SCORING**: Total must be 100 max. Redistribute weights: + - 方向: 35 + - 拥挤: 20 + - FR: 5 (within the 100) + - 环境: 15 + - 确认: 15 + - 清算: 5 (within the 100) + - 辅助: 5 + - Total = 100 + + **WAIT** - Don't change backend scoring yet. Just create the page. The scoring fix needs boss approval. + +### 3. V5.1 Paper Trading Page - RESTORE +- `/paper` should be V5.1 ONLY +- Remove strategy tabs, strategy badges, FR/Liq display +- Show ONLY v51_baseline trades +- Restore to clean original design + +### 4. V5.2 Paper Trading Page - NEW independent page +- Create `/paper-v52` page +- Show ONLY v52_8signals trades +- Include FR/Liq scores, strategy badge + +### 5. Sidebar Navigation +``` +🏠 仪表盘 +📊 成交流 + +── V5.1 ── +🎯 V5.1 信号引擎 +📈 V5.1 模拟盘 + +── V5.2 ── +✨ V5.2 信号引擎 +📈 V5.2 模拟盘 + +🖥️ 服务器 +ℹ️ 说明 +``` + +### 6. Key Files +- `frontend/app/signals/page.tsx` — restore to V5.1 only +- `frontend/app/signals-v52/page.tsx` — NEW, copy and modify for V5.2 +- `frontend/app/paper/page.tsx` — restore to V5.1 only (filter strategy=v51_baseline) +- `frontend/app/paper-v52/page.tsx` — NEW, V5.2 only (filter strategy=v52_8signals) +- `frontend/components/Sidebar.tsx` — update navigation + +### 7. DO NOT TOUCH backend/signal_engine.py or backend/main.py +The backend is fine. Only frontend changes. + +### 8. Test +```bash +cd frontend && npm run build +``` +Must pass with 0 errors. + +When done, run: +```bash +cd /root/Projects/arbitrage-engine && git add -A && git commit -m "refactor: completely separate V5.1 and V5.2 pages" +``` diff --git a/backend/strategies/v52_8signals.json b/backend/strategies/v52_8signals.json index ca07bf9..d405dab 100644 --- a/backend/strategies/v52_8signals.json +++ b/backend/strategies/v52_8signals.json @@ -4,12 +4,14 @@ "threshold": 75, "weights": { "direction": 40, - "crowding": 25, - "environment": 15, - "confirmation": 20, + "crowding": 18, + "funding_rate": 5, + "environment": 12, + "confirmation": 15, + "liquidation": 5, "auxiliary": 5 }, - "accel_bonus": 5, + "accel_bonus": 0, "tp_sl": { "sl_multiplier": 3.0, "tp1_multiplier": 2.0, diff --git a/frontend/app/paper-v52/page.tsx b/frontend/app/paper-v52/page.tsx new file mode 100644 index 0000000..893a79b --- /dev/null +++ b/frontend/app/paper-v52/page.tsx @@ -0,0 +1,657 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { authFetch, useAuth } from "@/lib/auth"; +import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts"; + +// ─── 工具函数 ──────────────────────────────────────────────────── + +function bjt(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; +} + +function fmtPrice(p: number) { + return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); +} + +function parseFactors(raw: any) { + if (!raw) return null; + if (typeof raw === "string") { + try { + return JSON.parse(raw); + } catch { + return null; + } + } + return raw; +} + +type StrategyFilter = "v52_8signals"; +const PAPER_STRATEGY: StrategyFilter = "v52_8signals"; + +function strategyBadgeClass() { + return "bg-emerald-100 text-emerald-700 border border-emerald-200"; +} + +function strategyBadgeText() { + return "✨ V5.2"; +} + +// ─── 控制面板(开关+配置)────────────────────────────────────── + +function ControlPanel() { + const [config, setConfig] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch("/api/paper/config"); + if (r.ok) setConfig(await r.json()); + } catch {} + }; + f(); + }, []); + + const toggle = async () => { + setSaving(true); + try { + const r = await authFetch("/api/paper/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: !config.enabled }), + }); + if (r.ok) setConfig(await r.json().then((j) => j.config)); + } catch { + } finally { + setSaving(false); + } + }; + + if (!config) return null; + + return ( +
+
+ + + {config.enabled ? "🟢 运行中" : "⚪ 已停止"} + +
+
+ 初始资金: ${config.initial_balance?.toLocaleString()} + 单笔风险: {(config.risk_per_trade * 100).toFixed(0)}% + 最大持仓: {config.max_positions} +
+
+ ); +} + +// ─── 总览面板 ──────────────────────────────────────────────────── + +function SummaryCards({ strategy }: { strategy: StrategyFilter }) { + const [data, setData] = useState(null); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch(`/api/paper/summary?strategy=${strategy}`); + if (r.ok) setData(await r.json()); + } catch {} + }; + f(); + const iv = setInterval(f, 10000); + return () => clearInterval(iv); + }, [strategy]); + + if (!data) return
加载中...
; + + return ( +
+
+

当前资金

+

= 10000 ? "text-emerald-600" : "text-red-500"}`}> + ${data.balance?.toLocaleString()} +

+
+
+

总盈亏(R)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {data.total_pnl >= 0 ? "+" : ""} + {data.total_pnl}R +

+

= 0 ? "text-emerald-500" : "text-red-400"}`}> + {data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt} +

+
+
+

胜率

+

{data.win_rate}%

+
+
+

总交易

+

{data.total_trades}

+
+
+

持仓中

+

{data.active_positions}

+
+
+

盈亏比(PF)

+

{data.profit_factor}

+
+
+

运行

+

{data.start_time ? "运行中 ✅" : "等待首笔"}

+
+
+ ); +} + +// ─── 最新信号状态 ──────────────────────────────────────────────── + +const COINS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]; + +function LatestSignals() { + const [signals, setSignals] = useState>({}); + useEffect(() => { + const f = async () => { + for (const sym of COINS) { + try { + const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT", "")}&limit=1`); + if (r.ok) { + const j = await r.json(); + if (j.data && j.data.length > 0) { + setSignals((prev) => ({ ...prev, [sym]: j.data[0] })); + } + } + } catch {} + } + }; + f(); + const iv = setInterval(f, 15000); + return () => clearInterval(iv); + }, []); + + return ( +
+
+

最新信号

+
+
+ {COINS.map((sym) => { + const s = signals[sym]; + const coin = sym.replace("USDT", ""); + const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null; + return ( +
+
+ {coin} + {s?.signal ? ( + <> + + {s.signal === "LONG" ? "🟢" : "🔴"} {s.signal} + + {s.score}分 + + ) : ( + ⚪ 无信号 + )} +
+ {ago !== null && {ago < 60 ? `${ago}m前` : `${Math.round(ago / 60)}h前`}} +
+ ); + })} +
+
+ ); +} + +// ─── 当前持仓 ──────────────────────────────────────────────────── + +function ActivePositions({ strategy }: { strategy: StrategyFilter }) { + const [positions, setPositions] = useState([]); + const [wsPrices, setWsPrices] = useState>({}); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch(`/api/paper/positions?strategy=${strategy}`); + if (r.ok) { + const j = await r.json(); + setPositions(j.data || []); + } + } catch {} + }; + f(); + const iv = setInterval(f, 10000); + return () => clearInterval(iv); + }, [strategy]); + + useEffect(() => { + const streams = ["btcusdt", "ethusdt", "xrpusdt", "solusdt"].map((s) => `${s}@aggTrade`).join("/"); + const ws = new WebSocket(`wss://fstream.binance.com/stream?streams=${streams}`); + ws.onmessage = (e) => { + try { + const msg = JSON.parse(e.data); + if (msg.data) { + const sym = msg.data.s; + const price = parseFloat(msg.data.p); + if (sym && price > 0) setWsPrices((prev) => ({ ...prev, [sym]: price })); + } + } catch {} + }; + return () => ws.close(); + }, []); + + if (positions.length === 0) + return ( +
+ V5.2 暂无活跃持仓 +
+ ); + + return ( +
+
+

+ 当前持仓 ● 实时 +

+
+
+ {positions.map((p: any) => { + const sym = p.symbol?.replace("USDT", "") || ""; + const holdMin = Math.round((Date.now() - p.entry_ts) / 60000); + const currentPrice = wsPrices[p.symbol] || p.current_price || 0; + const factors = parseFactors(p.score_factors); + const frScore = factors?.funding_rate?.score ?? 0; + const liqScore = factors?.liquidation?.score ?? 0; + const entry = p.entry_price || 0; + const atr = p.atr_at_entry || 1; + const riskDist = 2.0 * 0.7 * atr; + const fullR = riskDist > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / riskDist : (entry - currentPrice) / riskDist) : 0; + const tp1R = riskDist > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / riskDist : (entry - (p.tp1_price || 0)) / riskDist) : 0; + const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR; + const unrealUsdt = unrealR * 200; + return ( +
+
+
+ + {p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction} + + + {strategyBadgeText()} + + 评分{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"} + FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore} +
+
+ = 0 ? "text-emerald-600" : "text-red-500"}`}> + {unrealR >= 0 ? "+" : ""} + {unrealR.toFixed(2)}R + + = 0 ? "text-emerald-500" : "text-red-400"}`}> + ({unrealUsdt >= 0 ? "+" : ""}${unrealUsdt.toFixed(0)}) + + {holdMin}m +
+
+
+ 入场: ${fmtPrice(p.entry_price)} + 现价: ${currentPrice ? fmtPrice(currentPrice) : "-"} + TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""} + TP2: ${fmtPrice(p.tp2_price)} + SL: ${fmtPrice(p.sl_price)} +
+
+
FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore}
+
+
+ ); + })} +
+
+ ); +} + +// ─── 权益曲线 ──────────────────────────────────────────────────── + +function EquityCurve({ strategy }: { strategy: StrategyFilter }) { + const [data, setData] = useState([]); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch(`/api/paper/equity-curve?strategy=${strategy}`); + if (r.ok) { + const j = await r.json(); + setData(j.data || []); + } + } catch {} + }; + f(); + const iv = setInterval(f, 30000); + return () => clearInterval(iv); + }, [strategy]); + + return ( +
+
+

权益曲线 (累计PnL)

+
+ {data.length < 2 ? ( +
V5.2 暂无足够历史数据
+ ) : ( +
+ + + bjt(v)} tick={{ fontSize: 10 }} /> + `${v}R`} /> + bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} /> + + + + +
+ )} +
+ ); +} + +// ─── 历史交易列表 ──────────────────────────────────────────────── + +type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL"; +type FilterResult = "all" | "win" | "loss"; + +function TradeHistory({ strategy }: { strategy: StrategyFilter }) { + const [trades, setTrades] = useState([]); + const [symbol, setSymbol] = useState("all"); + const [result, setResult] = useState("all"); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch(`/api/paper/trades?symbol=${symbol}&result=${result}&strategy=${strategy}&limit=50`); + if (r.ok) { + const j = await r.json(); + setTrades(j.data || []); + } + } catch {} + }; + f(); + const iv = setInterval(f, 10000); + return () => clearInterval(iv); + }, [symbol, result, strategy]); + + return ( +
+
+

历史交易

+
+ + {strategyBadgeText()} 视图 + + | + {(["all", "BTC", "ETH", "XRP", "SOL"] as FilterSymbol[]).map((s) => ( + + ))} + | + {(["all", "win", "loss"] as FilterResult[]).map((r) => ( + + ))} +
+
+
+ {trades.length === 0 ? ( +
暂无交易记录
+ ) : ( + + + + + + + + + + + + + + + + {trades.map((t: any) => { + const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0; + const factors = parseFactors(t.score_factors); + const frScore = factors?.funding_rate?.score ?? 0; + const liqScore = factors?.liquidation?.score ?? 0; + return ( + + + + + + + + + + + + ); + })} + +
币种策略方向入场出场PnL(R)状态分数时间
{t.symbol?.replace("USDT", "")} + {strategyBadgeText()} + + {t.direction === "LONG" ? "🟢" : "🔴"} {t.direction} + {fmtPrice(t.entry_price)}{t.exit_price ? fmtPrice(t.exit_price) : "-"} 0 ? "text-emerald-600" : t.pnl_r < 0 ? "text-red-500" : "text-slate-500"}`}> + {t.pnl_r > 0 ? "+" : ""} + {t.pnl_r?.toFixed(2)} + + + {t.status === "tp" + ? "止盈" + : t.status === "sl" + ? "止损" + : t.status === "sl_be" + ? "保本" + : t.status === "timeout" + ? "超时" + : t.status === "signal_flip" + ? "翻转" + : t.status} + + +
{t.score}(FR{frScore >= 0 ? "+" : ""}{frScore}/Liq{liqScore >= 0 ? "+" : ""}{liqScore})
+
{holdMin}m
+ )} +
+
+ ); +} + +// ─── 统计面板 ──────────────────────────────────────────────────── + +function StatsPanel({ strategy }: { strategy: StrategyFilter }) { + const [data, setData] = useState(null); + const [tab, setTab] = useState("ALL"); + + useEffect(() => { + const f = async () => { + try { + const r = await authFetch(`/api/paper/stats?strategy=${strategy}`); + if (r.ok) setData(await r.json()); + } catch {} + }; + f(); + const iv = setInterval(f, 30000); + return () => clearInterval(iv); + }, [strategy]); + + useEffect(() => { + setTab("ALL"); + }, [strategy]); + + if (!data || data.error) { + return ( +
+
+

详细统计

+
+
该视图暂无统计数据
+
+ ); + } + + const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"]; + const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null); + + return ( +
+
+

详细统计

+
+ {strategyBadgeText()} + {tabs.map((t) => ( + + ))} +
+
+ {st ? ( +
+
+
+ 胜率 +

{st.win_rate}%

+
+
+ 盈亏比 +

{st.win_loss_ratio}

+
+
+ 平均盈利 +

+{st.avg_win}R

+
+
+ 平均亏损 +

-{st.avg_loss}R

+
+
+ 最大回撤 +

{st.mdd}R

+
+
+ 夏普比率 +

{st.sharpe}

+
+
+ 总盈亏 +

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {(st.total_pnl ?? 0) >= 0 ? "+" : ""} + {st.total_pnl ?? "-"}R +

+
+
+ 总笔数 +

{st.total ?? data.total}

+
+
+ 做多胜率 +

{st.long_win_rate}% ({st.long_count}笔)

+
+
+ 做空胜率 +

{st.short_win_rate}% ({st.short_count}笔)

+
+ {tab === "ALL" && data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => ( +
+ {t === "heavy" ? "加仓档" : t === "standard" ? "标准档" : "轻仓档"} +

{v.win_rate}% ({v.total}笔)

+
+ ))} +
+
+ ) : ( +
该币种暂无数据
+ )} +
+ ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── + +export default function PaperTradingV52Page() { + const { isLoggedIn, loading } = useAuth(); + const strategy: StrategyFilter = PAPER_STRATEGY; + + if (loading) return
加载中...
; + + if (!isLoggedIn) + return ( +
+
🔒
+

请先登录查看模拟盘

+ + 登录 + +
+ ); + + return ( +
+
+

📈 V5.2 模拟盘

+

仅展示 v52_8signals · 包含 FR / 清算评分与策略标签

+
+ + + + + + + + +
+ ); +} diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index 1b39ca8..7b6ddee 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -1,7 +1,5 @@ "use client"; - -import { useState, useEffect, Suspense } from "react"; -import { useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { authFetch, useAuth } from "@/lib/auth"; import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts"; @@ -17,53 +15,7 @@ function fmtPrice(p: number) { return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); } -function parseFactors(raw: any) { - if (!raw) return null; - if (typeof raw === "string") { - try { - return JSON.parse(raw); - } catch { - return null; - } - } - return raw; -} - -type StrategyFilter = "all" | "v51_baseline" | "v52_8signals"; - -const STRATEGY_TABS: { value: StrategyFilter; label: string; hint: string }[] = [ - { value: "all", label: "全部", hint: "总览" }, - { value: "v51_baseline", label: "V5.1 模拟盘", hint: "经典五层" }, - { value: "v52_8signals", label: "V5.2 模拟盘", hint: "8信号 + FR/Liq" }, -]; - -function normalizeStrategy(strategy: string | null | undefined): StrategyFilter { - if (strategy === "v52_8signals") return "v52_8signals"; - if (strategy === "v51_baseline") return "v51_baseline"; - return "v51_baseline"; -} - -function strategyName(strategy: string | null | undefined) { - const normalized = normalizeStrategy(strategy); - if (normalized === "v52_8signals") return "V5.2"; - return "V5.1"; -} - -function strategyBadgeClass(strategy: string | null | undefined) { - return normalizeStrategy(strategy) === "v52_8signals" - ? "bg-emerald-100 text-emerald-700 border border-emerald-200" - : "bg-slate-200 text-slate-700 border border-slate-300"; -} - -function strategyBadgeText(strategy: string | null | undefined) { - return normalizeStrategy(strategy) === "v52_8signals" ? "✨ V5.2" : "V5.1"; -} - -function strategyTabDescription(strategy: StrategyFilter) { - if (strategy === "all") return "全部策略合并视图"; - if (strategy === "v52_8signals") return "仅展示 V5.2 数据(含 FR / Liq)"; - return "仅展示 V5.1 数据"; -} +const PAPER_STRATEGY = "v51_baseline"; // ─── 控制面板(开关+配置)────────────────────────────────────── @@ -72,12 +24,7 @@ function ControlPanel() { const [saving, setSaving] = useState(false); useEffect(() => { - const f = async () => { - try { - const r = await authFetch("/api/paper/config"); - if (r.ok) setConfig(await r.json()); - } catch {} - }; + const f = async () => { try { const r = await authFetch("/api/paper/config"); if (r.ok) setConfig(await r.json()); } catch {} }; f(); }, []); @@ -89,11 +36,8 @@ function ControlPanel() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: !config.enabled }), }); - if (r.ok) setConfig(await r.json().then((j) => j.config)); - } catch { - } finally { - setSaving(false); - } + if (r.ok) setConfig(await r.json().then(j => j.config)); + } catch {} finally { setSaving(false); } }; if (!config) return null; @@ -101,13 +45,12 @@ function ControlPanel() { return (
- @@ -125,40 +68,23 @@ function ControlPanel() { // ─── 总览面板 ──────────────────────────────────────────────────── -function SummaryCards({ strategy }: { strategy: StrategyFilter }) { +function SummaryCards() { const [data, setData] = useState(null); - useEffect(() => { - const f = async () => { - try { - const r = await authFetch(`/api/paper/summary?strategy=${strategy}`); - if (r.ok) setData(await r.json()); - } catch {} - }; - f(); - const iv = setInterval(f, 10000); - return () => clearInterval(iv); - }, [strategy]); - + const f = async () => { try { const r = await authFetch(`/api/paper/summary?strategy=${PAPER_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); + }, []); if (!data) return
加载中...
; - return (

当前资金

-

= 10000 ? "text-emerald-600" : "text-red-500"}`}> - ${data.balance?.toLocaleString()} -

+

= 10000 ? "text-emerald-600" : "text-red-500"}`}>${data.balance?.toLocaleString()}

总盈亏(R)

-

= 0 ? "text-emerald-600" : "text-red-500"}`}> - {data.total_pnl >= 0 ? "+" : ""} - {data.total_pnl}R -

-

= 0 ? "text-emerald-500" : "text-red-400"}`}> - {data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt} -

+

= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl >= 0 ? "+" : ""}{data.total_pnl}R

+

= 0 ? "text-emerald-500" : "text-red-400"}`}>{data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt}

胜率

@@ -194,19 +120,17 @@ function LatestSignals() { const f = async () => { for (const sym of COINS) { try { - const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT", "")}&limit=1`); + const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT","")}&limit=1`); if (r.ok) { const j = await r.json(); if (j.data && j.data.length > 0) { - setSignals((prev) => ({ ...prev, [sym]: j.data[0] })); + setSignals(prev => ({ ...prev, [sym]: j.data[0] })); } } } catch {} } }; - f(); - const iv = setInterval(f, 15000); - return () => clearInterval(iv); + f(); const iv = setInterval(f, 15000); return () => clearInterval(iv); }, []); return ( @@ -215,7 +139,7 @@ function LatestSignals() {

最新信号

- {COINS.map((sym) => { + {COINS.map(sym => { const s = signals[sym]; const coin = sym.replace("USDT", ""); const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null; @@ -234,7 +158,7 @@ function LatestSignals() { ⚪ 无信号 )}
- {ago !== null && {ago < 60 ? `${ago}m前` : `${Math.round(ago / 60)}h前`}} + {ago !== null && {ago < 60 ? `${ago}m前` : `${Math.round(ago/60)}h前`}}
); })} @@ -245,27 +169,19 @@ function LatestSignals() { // ─── 当前持仓 ──────────────────────────────────────────────────── -function ActivePositions({ strategy }: { strategy: StrategyFilter }) { +function ActivePositions() { const [positions, setPositions] = useState([]); const [wsPrices, setWsPrices] = useState>({}); + // 从API获取持仓列表(10秒刷新) useEffect(() => { - const f = async () => { - try { - const r = await authFetch(`/api/paper/positions?strategy=${strategy}`); - if (r.ok) { - const j = await r.json(); - setPositions(j.data || []); - } - } catch {} - }; - f(); - const iv = setInterval(f, 10000); - return () => clearInterval(iv); - }, [strategy]); + const f = async () => { try { const r = await authFetch(`/api/paper/positions?strategy=${PAPER_STRATEGY}`); if (r.ok) { const j = await r.json(); setPositions(j.data || []); } } catch {} }; + f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); + }, []); + // WebSocket实时价格(aggTrade逐笔成交) useEffect(() => { - const streams = ["btcusdt", "ethusdt", "xrpusdt", "solusdt"].map((s) => `${s}@aggTrade`).join("/"); + const streams = ["btcusdt", "ethusdt", "xrpusdt", "solusdt"].map(s => `${s}@aggTrade`).join("/"); const ws = new WebSocket(`wss://fstream.binance.com/stream?streams=${streams}`); ws.onmessage = (e) => { try { @@ -273,62 +189,49 @@ function ActivePositions({ strategy }: { strategy: StrategyFilter }) { if (msg.data) { const sym = msg.data.s; const price = parseFloat(msg.data.p); - if (sym && price > 0) setWsPrices((prev) => ({ ...prev, [sym]: price })); + if (sym && price > 0) setWsPrices(prev => ({ ...prev, [sym]: price })); } } catch {} }; return () => ws.close(); }, []); - if (positions.length === 0) - return ( -
- {strategy === "all" ? "暂无活跃持仓" : `${strategyName(strategy)} 暂无活跃持仓`} -
- ); + if (positions.length === 0) return ( +
+ 暂无活跃持仓 +
+ ); return (
-

- 当前持仓 ● 实时 -

+

当前持仓 ● 实时

{positions.map((p: any) => { const sym = p.symbol?.replace("USDT", "") || ""; const holdMin = Math.round((Date.now() - p.entry_ts) / 60000); const currentPrice = wsPrices[p.symbol] || p.current_price || 0; - const factors = parseFactors(p.score_factors); - const frScore = factors?.funding_rate?.score ?? 0; - const liqScore = factors?.liquidation?.score ?? 0; const entry = p.entry_price || 0; const atr = p.atr_at_entry || 1; const riskDist = 2.0 * 0.7 * atr; + // TP1触发后只剩半仓:0.5×TP1锁定 + 0.5×当前浮盈 const fullR = riskDist > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / riskDist : (entry - currentPrice) / riskDist) : 0; const tp1R = riskDist > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / riskDist : (entry - (p.tp1_price || 0)) / riskDist) : 0; const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR; const unrealUsdt = unrealR * 200; - const isV52 = normalizeStrategy(p.strategy) === "v52_8signals"; return ( -
-
-
+
+
+
{p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction} - - {strategyBadgeText(p.strategy)} - - 评分{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"} - {isV52 && ( - FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore} - )} + 评分{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"}
= 0 ? "text-emerald-600" : "text-red-500"}`}> - {unrealR >= 0 ? "+" : ""} - {unrealR.toFixed(2)}R + {unrealR >= 0 ? "+" : ""}{unrealR.toFixed(2)}R = 0 ? "text-emerald-500" : "text-red-400"}`}> ({unrealUsdt >= 0 ? "+" : ""}${unrealUsdt.toFixed(0)}) @@ -336,19 +239,13 @@ function ActivePositions({ strategy }: { strategy: StrategyFilter }) { {holdMin}m
-
+
入场: ${fmtPrice(p.entry_price)} 现价: ${currentPrice ? fmtPrice(currentPrice) : "-"} TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""} TP2: ${fmtPrice(p.tp2_price)} SL: ${fmtPrice(p.sl_price)} - {!isV52 && FR/Liq 仅 V5.2 显示}
- {isV52 && ( -
- {isV52 &&
FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore}
} -
- )}
); })} @@ -359,44 +256,31 @@ function ActivePositions({ strategy }: { strategy: StrategyFilter }) { // ─── 权益曲线 ──────────────────────────────────────────────────── -function EquityCurve({ strategy }: { strategy: StrategyFilter }) { +function EquityCurve() { const [data, setData] = useState([]); - useEffect(() => { - const f = async () => { - try { - const r = await authFetch(`/api/paper/equity-curve?strategy=${strategy}`); - if (r.ok) { - const j = await r.json(); - setData(j.data || []); - } - } catch {} - }; - f(); - const iv = setInterval(f, 30000); - return () => clearInterval(iv); - }, [strategy]); + const f = async () => { try { const r = await authFetch(`/api/paper/equity-curve?strategy=${PAPER_STRATEGY}`); if (r.ok) { const j = await r.json(); setData(j.data || []); } } catch {} }; + f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); + }, []); + + if (data.length < 2) return null; return (

权益曲线 (累计PnL)

- {data.length < 2 ? ( -
{strategy === "all" ? "暂无足够历史数据" : `${strategyName(strategy)} 暂无足够历史数据`}
- ) : ( -
- - - bjt(v)} tick={{ fontSize: 10 }} /> - `${v}R`} /> - bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} /> - - - - -
- )} +
+ + + bjt(v)} tick={{ fontSize: 10 }} /> + `${v}R`} /> + bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} /> + + + + +
); } @@ -406,7 +290,7 @@ function EquityCurve({ strategy }: { strategy: StrategyFilter }) { type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL"; type FilterResult = "all" | "win" | "loss"; -function TradeHistory({ strategy }: { strategy: StrategyFilter }) { +function TradeHistory() { const [trades, setTrades] = useState([]); const [symbol, setSymbol] = useState("all"); const [result, setResult] = useState("all"); @@ -414,43 +298,28 @@ function TradeHistory({ strategy }: { strategy: StrategyFilter }) { useEffect(() => { const f = async () => { try { - const r = await authFetch(`/api/paper/trades?symbol=${symbol}&result=${result}&strategy=${strategy}&limit=50`); - if (r.ok) { - const j = await r.json(); - setTrades(j.data || []); - } + const r = await authFetch(`/api/paper/trades?symbol=${symbol}&result=${result}&strategy=${PAPER_STRATEGY}&limit=50`); + if (r.ok) { const j = await r.json(); setTrades(j.data || []); } } catch {} }; - f(); - const iv = setInterval(f, 10000); - return () => clearInterval(iv); - }, [symbol, result, strategy]); + f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); + }, [symbol, result]); return (

历史交易

-
- - {strategy === "all" ? "全部策略" : `${strategyBadgeText(strategy)} 视图`} - - | - {(["all", "BTC", "ETH", "XRP", "SOL"] as FilterSymbol[]).map((s) => ( - ))} | - {(["all", "win", "loss"] as FilterResult[]).map((r) => ( - ))} @@ -464,7 +333,6 @@ function TradeHistory({ strategy }: { strategy: StrategyFilter }) { 币种 - 策略 方向 入场 出场 @@ -477,55 +345,29 @@ function TradeHistory({ strategy }: { strategy: StrategyFilter }) { {trades.map((t: any) => { const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0; - const factors = parseFactors(t.score_factors); - const frScore = factors?.funding_rate?.score ?? 0; - const liqScore = factors?.liquidation?.score ?? 0; - const isV52 = normalizeStrategy(t.strategy) === "v52_8signals"; return ( {t.symbol?.replace("USDT", "")} - - {strategyBadgeText(t.strategy)} - {t.direction === "LONG" ? "🟢" : "🔴"} {t.direction} {fmtPrice(t.entry_price)} {t.exit_price ? fmtPrice(t.exit_price) : "-"} 0 ? "text-emerald-600" : t.pnl_r < 0 ? "text-red-500" : "text-slate-500"}`}> - {t.pnl_r > 0 ? "+" : ""} - {t.pnl_r?.toFixed(2)} + {t.pnl_r > 0 ? "+" : ""}{t.pnl_r?.toFixed(2)} - - {t.status === "tp" - ? "止盈" - : t.status === "sl" - ? "止损" - : t.status === "sl_be" - ? "保本" - : t.status === "timeout" - ? "超时" - : t.status === "signal_flip" - ? "翻转" - : t.status} + + {t.status === "tp" ? "止盈" : t.status === "sl" ? "止损" : t.status === "sl_be" ? "保本" : t.status === "timeout" ? "超时" : t.status === "signal_flip" ? "翻转" : t.status} - -
{t.score}{isV52 ? (FR{frScore >= 0 ? "+" : ""}{frScore}/Liq{liqScore >= 0 ? "+" : ""}{liqScore}) : ""}
- + {t.score} {holdMin}m ); @@ -540,36 +382,15 @@ function TradeHistory({ strategy }: { strategy: StrategyFilter }) { // ─── 统计面板 ──────────────────────────────────────────────────── -function StatsPanel({ strategy }: { strategy: StrategyFilter }) { +function StatsPanel() { const [data, setData] = useState(null); const [tab, setTab] = useState("ALL"); - useEffect(() => { - const f = async () => { - try { - const r = await authFetch(`/api/paper/stats?strategy=${strategy}`); - if (r.ok) setData(await r.json()); - } catch {} - }; - f(); - const iv = setInterval(f, 30000); - return () => clearInterval(iv); - }, [strategy]); + const f = async () => { try { const r = await authFetch(`/api/paper/stats?strategy=${PAPER_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); + }, []); - useEffect(() => { - setTab("ALL"); - }, [strategy]); - - if (!data || data.error) { - return ( -
-
-

详细统计

-
-
该视图暂无统计数据
-
- ); - } + if (!data || data.error) return null; const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"]; const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null); @@ -578,72 +399,29 @@ function StatsPanel({ strategy }: { strategy: StrategyFilter }) {

详细统计

-
- {strategy !== "all" && {strategyBadgeText(strategy)}} - {tabs.map((t) => ( - + >{t === "ALL" ? "总计" : t} ))}
{st ? ( -
-
-
- 胜率 -

{st.win_rate}%

-
-
- 盈亏比 -

{st.win_loss_ratio}

-
-
- 平均盈利 -

+{st.avg_win}R

-
-
- 平均亏损 -

-{st.avg_loss}R

-
-
- 最大回撤 -

{st.mdd}R

-
-
- 夏普比率 -

{st.sharpe}

-
-
- 总盈亏 -

= 0 ? "text-emerald-600" : "text-red-500"}`}> - {(st.total_pnl ?? 0) >= 0 ? "+" : ""} - {st.total_pnl ?? "-"}R -

-
-
- 总笔数 -

{st.total ?? data.total}

-
-
- 做多胜率 -

{st.long_win_rate}% ({st.long_count}笔)

-
-
- 做空胜率 -

{st.short_win_rate}% ({st.short_count}笔)

-
- {tab === "ALL" && data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => ( -
- {t === "heavy" ? "加仓档" : t === "standard" ? "标准档" : "轻仓档"} -

{v.win_rate}% ({v.total}笔)

-
- ))} -
+
+
胜率

{st.win_rate}%

+
盈亏比

{st.win_loss_ratio}

+
平均盈利

+{st.avg_win}R

+
平均亏损

-{st.avg_loss}R

+
最大回撤

{st.mdd}R

+
夏普比率

{st.sharpe}

+
总盈亏

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(st.total_pnl ?? 0) >= 0 ? "+" : ""}{st.total_pnl ?? "-"}R

+
总笔数

{st.total ?? data.total}

+
做多胜率

{st.long_win_rate}% ({st.long_count}笔)

+
做空胜率

{st.short_win_rate}% ({st.short_count}笔)

+ {tab === "ALL" && data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => ( +
{t === "heavy" ? "加仓档" : t === "standard" ? "标准档" : "轻仓档"}

{v.win_rate}% ({v.total}笔)

+ ))}
) : (
该币种暂无数据
@@ -654,74 +432,33 @@ function StatsPanel({ strategy }: { strategy: StrategyFilter }) { // ─── 主页面 ────────────────────────────────────────────────────── -function PaperTradingPageInner() { +export default function PaperTradingPage() { const { isLoggedIn, loading } = useAuth(); - 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
加载中...
; - if (!isLoggedIn) - return ( -
-
🔒
-

请先登录查看模拟盘

- - 登录 - -
- ); + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看模拟盘

+ 登录 +
+ ); return (
-
-

策略视图(顶部切换)

-
- {STRATEGY_TABS.map((tab) => ( - - ))} -
-
-
-

📊 模拟盘

-

V5.2策略AB测试 · 实时追踪 · 数据驱动优化 · {strategyTabDescription(strategyTab)}

+

📈 V5.1 模拟盘

+

仅展示 v51_baseline · V5.1信号引擎自动交易 · 实时追踪

- + - - - - + + + +
); } - -export default function PaperTradingPage() { - return ( - 加载中...
}> - - - ); -} diff --git a/frontend/app/signals-v52/page.tsx b/frontend/app/signals-v52/page.tsx new file mode 100644 index 0000000..778c4c7 --- /dev/null +++ b/frontend/app/signals-v52/page.tsx @@ -0,0 +1,527 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { authFetch } from "@/lib/auth"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; +import { + ComposedChart, Area, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, + ReferenceLine, CartesianGrid, Legend +} from "recharts"; + +type Symbol = "BTC" | "ETH" | "XRP" | "SOL"; + +interface IndicatorRow { + ts: number; + cvd_fast: number; + cvd_mid: number; + cvd_day: number; + atr_5m: number; + vwap_30m: number; + price: number; + score: number; + signal: string | null; +} + +interface LatestIndicator { + ts: number; + cvd_fast: number; + cvd_mid: number; + cvd_day: number; + cvd_fast_slope: number; + atr_5m: number; + atr_percentile: number; + vwap_30m: number; + price: number; + p95_qty: number; + p99_qty: number; + score: number; + signal: string | null; + tier?: "light" | "standard" | "heavy" | null; + factors?: { + direction?: { score?: number }; + crowding?: { score?: number }; + environment?: { score?: number }; + confirmation?: { score?: number }; + auxiliary?: { score?: number }; + funding_rate?: { score?: number; value?: number }; + liquidation?: { score?: number; long_usd?: number; short_usd?: number }; + } | null; +} + +interface MarketIndicatorValue { + value: Record; + ts: number; +} + +interface MarketIndicatorSet { + long_short_ratio?: MarketIndicatorValue; + top_trader_position?: MarketIndicatorValue; + open_interest_hist?: MarketIndicatorValue; + coinbase_premium?: MarketIndicatorValue; +} + +const WINDOWS = [ + { label: "1h", value: 60 }, + { label: "4h", value: 240 }, + { label: "12h", value: 720 }, + { label: "24h", value: 1440 }, +]; + +function bjtStr(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; +} + +function fmt(v: number, decimals = 1): string { + if (Math.abs(v) >= 1000000) return `${(v / 1000000).toFixed(1)}M`; + if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}K`; + return v.toFixed(decimals); +} + +function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) { + const ratio = Math.max(0, Math.min((score / max) * 100, 100)); + return ( +
+ {label} +
+
+
+ {score}/{max} +
+ ); +} + +function MarketIndicatorsCards({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState(null); + + useEffect(() => { + const fetch = async () => { + try { + const res = await authFetch("/api/signals/market-indicators"); + if (!res.ok) return; + const json = await res.json(); + setData(json[symbol] || null); + } catch {} + }; + fetch(); + const iv = setInterval(fetch, 5000); + return () => clearInterval(iv); + }, [symbol]); + + if (!data) return
等待市场指标数据...
; + + // value可能是JSON字符串或对象,统一解析 + const parseVal = (v: unknown): Record => { + if (!v) return {}; + if (typeof v === "string") { try { return JSON.parse(v); } catch { return {}; } } + if (typeof v === "object") return v as Record; + return {}; + }; + + const lsVal = parseVal(data.long_short_ratio?.value); + const topVal = parseVal(data.top_trader_position?.value); + const oiVal = parseVal(data.open_interest_hist?.value); + const premVal = parseVal(data.coinbase_premium?.value); + + const longPct = Number(lsVal?.longAccount ?? 0.5) * 100; + const shortPct = Number(lsVal?.shortAccount ?? 0.5) * 100; + const topLong = Number(topVal?.longAccount ?? 0.5) * 100; + const topShort = Number(topVal?.shortAccount ?? 0.5) * 100; + const oiValue = Number(oiVal?.sumOpenInterestValue ?? 0); + const oiDisplay = oiValue >= 1e9 ? `$${(oiValue / 1e9).toFixed(2)}B` : oiValue >= 1e6 ? `$${(oiValue / 1e6).toFixed(0)}M` : `$${oiValue.toFixed(0)}`; + const premium = Number(premVal?.premium_pct ?? 0); + + return ( +
+
+

多空比

+

L:{longPct.toFixed(1)}% S:{shortPct.toFixed(1)}%

+
+
+

大户持仓

+

多{topLong.toFixed(1)}% {topLong >= 55 ? "📈" : topLong <= 45 ? "📉" : "➖"}

+
+
+

OI

+

{oiDisplay}

+
+
+

CB Premium

+

= 0 ? "text-emerald-600" : "text-red-500"}`}>{premium >= 0 ? "+" : ""}{premium.toFixed(4)}%

+
+
+ ); +} + +// ─── 信号历史 ──────────────────────────────────────────────────── + +interface SignalRecord { + ts: number; + score: number; + signal: string; +} + +function bjtFull(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`; +} + +function SignalHistory({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20`); + if (!res.ok) return; + const json = await res.json(); + setData(json.data || []); + } catch {} + }; + fetchData(); + const iv = setInterval(fetchData, 15000); + return () => clearInterval(iv); + }, [symbol]); + + if (data.length === 0) return null; + + return ( +
+
+

最近信号

+
+
+ {data.map((s, i) => ( +
+
+ + {s.signal === "LONG" ? "🟢 LONG" : "🔴 SHORT"} + + {bjtFull(s.ts)} +
+
+ {s.score} + = 85 ? "bg-red-100 text-red-700" : + s.score >= 75 ? "bg-blue-100 text-blue-700" : + "bg-slate-100 text-slate-600" + }`}> + {s.score >= 85 ? "加仓" : s.score >= 75 ? "标准" : "轻仓"} + +
+
+ ))} +
+
+ ); +} + +// ─── 实时指标卡片 ──────────────────────────────────────────────── + +function IndicatorCards({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState(null); + + useEffect(() => { + const fetch = async () => { + try { + const res = await authFetch("/api/signals/latest"); + if (!res.ok) return; + const json = await res.json(); + setData(json[symbol] || null); + } catch {} + }; + fetch(); + const iv = setInterval(fetch, 5000); + return () => clearInterval(iv); + }, [symbol]); + + if (!data) return
等待指标数据...
; + + const cvdMidDir = data.cvd_mid > 0 ? "多" : "空"; + const priceVsVwap = data.price > data.vwap_30m ? "上方" : "下方"; + + return ( +
+ {/* CVD三轨 - 紧凑一行 */} +
+
+

CVD_fast (30m)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {fmt(data.cvd_fast)} +

+

+ 斜率: = 0 ? "text-emerald-600" : "text-red-500"}> + {data.cvd_fast_slope >= 0 ? "↑" : "↓"}{fmt(Math.abs(data.cvd_fast_slope))} + +

+
+
+

CVD_mid (4h)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {fmt(data.cvd_mid)} +

+

{cvdMidDir}头占优

+
+
+

CVD_day

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {fmt(data.cvd_day)} +

+

盘中基线

+
+
+ + {/* ATR + VWAP + 大单 - 4列紧凑 */} +
+
+

ATR

+

${fmt(data.atr_5m, 2)}

+

+ 60 ? "text-amber-600 font-semibold" : "text-slate-400"}> + {data.atr_percentile.toFixed(0)}%{data.atr_percentile > 60 ? "🔥" : ""} + +

+
+
+

VWAP

+

${data.vwap_30m.toLocaleString("en-US", { maximumFractionDigits: 1 })}

+

+ 价格在 data.vwap_30m ? "text-emerald-600" : "text-red-500"}>{priceVsVwap} +

+
+
+

P95

+

{data.p95_qty.toFixed(4)}

+

大单阈值

+
+
+

P99

+

{data.p99_qty.toFixed(4)}

+

超大单

+
+
+ + {/* 信号状态(V5.2)- 七层 */} +
+
+
+

当前信号

+

+ {data.signal === "LONG" ? "🟢 做多" : data.signal === "SHORT" ? "🔴 做空" : "⚪ 无信号"} +

+
+
+

{data.score}/100

+

{data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : data.tier === "light" ? "轻仓" : "不开仓"}

+
+
+
+ + + + + + + +
+
+
+ ); +} + +// ─── CVD三轨图 ────────────────────────────────────────────────── + +function CVDChart({ symbol, minutes }: { symbol: Symbol; minutes: number }) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchData = useCallback(async (silent = false) => { + try { + const res = await authFetch(`/api/signals/indicators?symbol=${symbol}&minutes=${minutes}`); + if (!res.ok) return; + const json = await res.json(); + setData(json.data || []); + if (!silent) setLoading(false); + } catch {} + }, [symbol, minutes]); + + useEffect(() => { + setLoading(true); + fetchData(); + const iv = setInterval(() => fetchData(true), 30000); + return () => clearInterval(iv); + }, [fetchData]); + + const chartData = data.map(d => ({ + time: bjtStr(d.ts), + fast: parseFloat(d.cvd_fast?.toFixed(2) || "0"), + mid: parseFloat(d.cvd_mid?.toFixed(2) || "0"), + price: d.price, + })); + + // 价格轴自适应 + const prices = chartData.map(d => d.price).filter(v => v > 0); + const pMin = prices.length ? Math.min(...prices) : 0; + const pMax = prices.length ? Math.max(...prices) : 0; + const pPad = (pMax - pMin) * 0.3 || pMax * 0.001; + + if (loading) return
加载指标数据...
; + if (data.length === 0) return
暂无指标数据,signal-engine需运行积累
; + + return ( + + + + + + v >= 1000 ? `$${(v / 1000).toFixed(1)}k` : `$${v.toFixed(0)}`} + /> + { + if (name === "price") return [`$${Number(v).toLocaleString()}`, "币价"]; + if (name === "fast") return [fmt(Number(v)), "CVD_fast(30m)"]; + return [fmt(Number(v)), "CVD_mid(4h)"]; + }} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} + /> + + + + + + + + ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── + +export default function SignalsV52Page() { + const { isLoggedIn, loading } = useAuth(); + const [symbol, setSymbol] = useState("BTC"); + const [minutes, setMinutes] = useState(240); + + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看信号数据

+
+ 登录 + 注册 +
+
+ ); + + return ( +
+ {/* 标题 */} +
+
+

⚡ 信号引擎 V5.2

+

七层信号评分 · 包含 Funding Rate / Liquidation 维度

+
+
+ {(["BTC", "ETH", "XRP", "SOL"] as Symbol[]).map(s => ( + + ))} +
+
+ + {/* 实时指标卡片 */} + + + {/* Market Indicators */} +
+

Market Indicators

+ +
+ + {/* 信号历史 */} + + + {/* CVD三轨图 */} +
+
+
+

CVD三轨 + 币价

+

蓝=fast(30m) · 紫=mid(4h) · 橙=价格

+
+
+ {WINDOWS.map(w => ( + + ))} +
+
+
+ +
+
+ + {/* 说明 */} +
+
+

📖 七层信号源说明(V5.2)

+
+
+
+ 1️⃣ 方向层(40分) + — 钱往哪流? +

CVD三轨(30m/4h资金流向)+ P99大单流(鲸鱼动向)+ 加速度奖励。两条CVD同向+大单配合 = 高分。

+
+
+ 2️⃣ 拥挤层(20分) + — 散户在干嘛?反着来 +

多空比(散户仓位)+ 大户持仓比。散户疯狂做多→做空加分,跟大户同向加分。

+
+
+ 3️⃣ FR层(5分) + — 费率拥挤是否极端? +

Funding Rate偏离越极端,反向信号权重越高;用于过滤高拥挤下的追涨杀跌。

+
+
+ 4️⃣ 环境层(15分) + — 有没有新钱进场? +

OI变化率(未平仓合约)。OI上涨=新资金进场=趋势延续;OI下降=资金撤离。

+
+
+ 5️⃣ 确认层(15分) + — 多周期共振吗? +

CVD_fast(30m)和CVD_mid(4h)方向一致=高确信度满分15;方向矛盾=0分。

+
+
+ 6️⃣ 清算层(5分) + — 清算热点是否共振? +

多空清算金额不对称时为反向提供弹性加权,帮助识别踩踏与挤空/挤多机会。

+
+
+ 7️⃣ 辅助层(5分) + — 美国机构在干嘛? +

Coinbase Premium(CB vs 币安价差)。正溢价=机构买入=做多加分;负溢价=机构卖出。

+
+
+ 档位:<60不开仓 · 60-74轻仓 · 75-84标准 · ≥85加仓 · 冷却10分钟 +
+
+
+
+ ); +} diff --git a/frontend/app/signals/page.tsx b/frontend/app/signals/page.tsx index 986ab3b..8e45496 100644 --- a/frontend/app/signals/page.tsx +++ b/frontend/app/signals/page.tsx @@ -44,28 +44,9 @@ interface LatestIndicator { environment?: { score?: number }; confirmation?: { score?: number }; auxiliary?: { score?: number }; - funding_rate?: { score?: number; value?: number }; - liquidation?: { score?: number; long_usd?: number; short_usd?: number }; } | null; } -interface StrategyScoreSnapshot { - score: number | null; - signal: string | null; - ts: number | null; - source?: string; - funding_rate_score?: number | null; - liquidation_score?: number | null; -} - -interface StrategyLatestRow { - primary_strategy?: "v51_baseline" | "v52_8signals"; - latest_signal?: string | null; - latest_ts?: number | null; - v51?: StrategyScoreSnapshot; - v52?: StrategyScoreSnapshot; -} - interface MarketIndicatorValue { value: Record; ts: number; @@ -100,14 +81,6 @@ function pct(v: number, digits = 1): string { return `${(v * 100).toFixed(digits)}%`; } -function agoLabel(ts: number | null | undefined): string { - if (!ts) return "--"; - const minutes = Math.round((Date.now() - ts) / 60000); - if (minutes < 1) return "刚刚"; - if (minutes < 60) return `${minutes}m前`; - return `${Math.round(minutes / 60)}h前`; -} - function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) { const ratio = Math.max(0, Math.min((score / max) * 100, 100)); return ( @@ -121,73 +94,6 @@ function LayerScore({ label, score, max, colorClass }: { label: string; score: n ); } -function LatestStrategyComparison() { - const [rows, setRows] = useState>({ - BTC: undefined, - ETH: undefined, - XRP: undefined, - SOL: undefined, - }); - - useEffect(() => { - const fetch = async () => { - try { - const res = await authFetch("/api/signals/latest-v52"); - if (!res.ok) return; - const json = await res.json(); - setRows({ - BTC: json.BTC, - ETH: json.ETH, - XRP: json.XRP, - SOL: json.SOL, - }); - } catch {} - }; - fetch(); - const iv = setInterval(fetch, 10000); - return () => clearInterval(iv); - }, []); - - return ( -
-
-

最新信号对比(V5.1 vs V5.2)

-
-
- {(["BTC", "ETH", "XRP", "SOL"] as Symbol[]).map((sym) => { - const row = rows[sym]; - const latestSignal = row?.latest_signal; - const v51 = row?.v51; - const v52 = row?.v52; - const v52Fr = v52?.funding_rate_score; - const v52Liq = v52?.liquidation_score; - return ( -
-
-

{sym}

- {agoLabel(row?.latest_ts ?? null)} -
-

- {latestSignal === "LONG" ? "🟢 LONG" : latestSignal === "SHORT" ? "🔴 SHORT" : "⚪ 无信号"} -

-
- V5.1: {v51?.score ?? "--"}分 - ✨ V5.2: {v52?.score ?? "--"}分 -
-
- {v52Fr === null || v52Fr === undefined ? "FR --" : `FR ${v52Fr >= 0 ? "+" : ""}${v52Fr}`} · {v52Liq === null || v52Liq === undefined ? "Liq --" : `Liq ${v52Liq >= 0 ? "+" : ""}${v52Liq}`} -
-
- 来源: V5.1 {v51?.source || "--"} | V5.2 {v52?.source || "--"} -
-
- ); - })} -
-
- ); -} - function MarketIndicatorsCards({ symbol }: { symbol: Symbol }) { const [data, setData] = useState(null); @@ -432,8 +338,6 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) { - -
@@ -532,8 +436,8 @@ export default function SignalsPage() { {/* 标题 */}
-

⚡ 信号引擎 V5.1 vs V5.2

-

并排评分对比 · V5.2 含 Funding Rate / Liquidation 额外维度

+

⚡ 信号引擎 V5.1

+

五层100分评分 · 市场拥挤度 · 环境确认

{(["BTC", "ETH", "XRP", "SOL"] as Symbol[]).map(s => ( @@ -545,8 +449,6 @@ export default function SignalsPage() {
- - {/* 实时指标卡片 */} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 33be8e5..457094e 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -7,16 +7,16 @@ import { useAuth } from "@/lib/auth"; import { LayoutDashboard, Info, Menu, X, Zap, LogIn, UserPlus, - ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles, FlaskConical + ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles } from "lucide-react"; const navItems = [ { href: "/", label: "仪表盘", icon: LayoutDashboard }, { href: "/trades", label: "成交流", icon: Activity }, - { 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: "/signals", label: "V5.1 信号引擎", icon: Crosshair, section: "── V5.1 ──" }, + { href: "/paper", label: "V5.1 模拟盘", icon: LineChart }, + { href: "/signals-v52", label: "V5.2 信号引擎", icon: Sparkles, section: "── V5.2 ──" }, + { href: "/paper-v52", label: "V5.2 模拟盘", icon: LineChart }, { href: "/server", label: "服务器", icon: Monitor }, { href: "/about", label: "说明", icon: Info }, ]; @@ -39,12 +39,12 @@ export default function Sidebar() { {/* Nav */}