From 7ba53a50052c2364dd19683c43d5adf43b3d49fc Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 11:55:03 +0000 Subject: [PATCH] Update paper UI for strategy filters and FR/liquidation details --- frontend/app/paper/page.tsx | 147 ++++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 21 deletions(-) diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index c7b6a14..14e88e7 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { authFetch, useAuth } from "@/lib/auth"; -import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts"; +import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts"; // ─── 工具函数 ──────────────────────────────────────────────────── @@ -15,6 +15,24 @@ 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; +} + +function strategyName(strategy: string | null | undefined) { + if (strategy === "v52_8signals") return "V5.2"; + if (strategy === "v51_baseline") return "V5.1"; + return strategy || "V5.1"; +} + // ─── 控制面板(开关+配置)────────────────────────────────────── function ControlPanel() { @@ -210,6 +228,9 @@ function ActivePositions() { 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; @@ -225,7 +246,9 @@ function ActivePositions() { {p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction} - 评分{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"} + + {strategyName(p.strategy)} · 评分{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"} +
= 0 ? "text-emerald-600" : "text-red-500"}`}> @@ -243,6 +266,8 @@ function ActivePositions() { 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}
); @@ -287,21 +312,23 @@ function EquityCurve() { type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL"; type FilterResult = "all" | "win" | "loss"; +type FilterStrategy = "all" | "v51_baseline" | "v52_8signals"; function TradeHistory() { const [trades, setTrades] = useState([]); const [symbol, setSymbol] = useState("all"); const [result, setResult] = useState("all"); + const [strategy, setStrategy] = useState("all"); useEffect(() => { const f = async () => { try { - const r = await authFetch(`/api/paper/trades?symbol=${symbol}&result=${result}&limit=50`); + 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]); + }, [symbol, result, strategy]); return (
@@ -321,6 +348,13 @@ function TradeHistory() { {r === "all" ? "全部" : r === "win" ? "盈利" : "亏损"} ))} + | + {(["all", "v51_baseline", "v52_8signals"] as FilterStrategy[]).map(s => ( + + ))}
@@ -331,6 +365,7 @@ function TradeHistory() { 币种 + 策略 方向 入场 出场 @@ -343,9 +378,19 @@ function TradeHistory() { {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 ( {t.symbol?.replace("USDT", "")} + + + {strategyName(t.strategy)} + + {t.direction === "LONG" ? "🟢" : "🔴"} {t.direction} @@ -365,7 +410,10 @@ function TradeHistory() { {t.status === "tp" ? "止盈" : t.status === "sl" ? "止损" : t.status === "sl_be" ? "保本" : t.status === "timeout" ? "超时" : t.status === "signal_flip" ? "翻转" : t.status} - {t.score} + +
{t.score}
+
FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore}
+ {holdMin}m ); @@ -383,8 +431,22 @@ function TradeHistory() { function StatsPanel() { const [data, setData] = useState(null); const [tab, setTab] = useState("ALL"); + const [strategyStats, setStrategyStats] = useState([]); + const [strategyTab, setStrategyTab] = useState<"all" | "v51_baseline" | "v52_8signals">("all"); useEffect(() => { - const f = async () => { try { const r = await authFetch("/api/paper/stats"); if (r.ok) setData(await r.json()); } catch {} }; + const f = async () => { + try { + const [statsRes, byStrategyRes] = await Promise.all([ + authFetch("/api/paper/stats"), + authFetch("/api/paper/stats-by-strategy"), + ]); + if (statsRes.ok) setData(await statsRes.json()); + if (byStrategyRes.ok) { + const j = await byStrategyRes.json(); + setStrategyStats(j.data || []); + } + } catch {} + }; f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); }, []); @@ -392,6 +454,20 @@ function StatsPanel() { const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"]; const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null); + const strategyView = strategyTab === "all" + ? (() => { + if (!strategyStats.length) return null; + const total = strategyStats.reduce((sum, s) => sum + (s.total || 0), 0); + const weightedWins = strategyStats.reduce((sum, s) => sum + (s.total || 0) * ((s.win_rate || 0) / 100), 0); + return { + strategy: "all", + total, + win_rate: total > 0 ? (weightedWins / total) * 100 : 0, + total_pnl: strategyStats.reduce((sum, s) => sum + (s.total_pnl || 0), 0), + active_positions: strategyStats.reduce((sum, s) => sum + (s.active_positions || 0), 0), + }; + })() + : (strategyStats.find((s) => s.strategy === strategyTab) || null); return (
@@ -406,20 +482,49 @@ function StatsPanel() {
{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}笔)

+ ))} +
+
+
+

策略对比

+
+ {(["all", "v51_baseline", "v52_8signals"] as const).map((s) => ( + + ))} +
+
+ {strategyView ? ( +
+
策略

{strategyView.strategy === "all" ? "ALL" : strategyName(strategyView.strategy)}

+
胜率

{(strategyView.win_rate || 0).toFixed(1)}%

+
总笔数

{strategyView.total || 0}

+
活跃仓位

{strategyView.active_positions || 0}

+
总盈亏

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

+
+ ) : ( +
暂无策略统计
+ )} +
) : (
该币种暂无数据
@@ -447,7 +552,7 @@ export default function PaperTradingPage() {

📊 模拟盘

-

V5.1信号引擎自动交易 · 实时追踪 · 数据驱动优化

+

V5.2策略AB测试 · 实时追踪 · 数据驱动优化