From bda42e669aa85470e4629aa6626208a870ab6b96 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 1 Mar 2026 01:37:02 +0000 Subject: [PATCH] feat: stats panel with per-symbol tabs (ALL/BTC/ETH/XRP/SOL) - full stats for each coin --- backend/main.py | 48 ++++++++++++++++++++++++++++++++----- frontend/app/paper/page.tsx | 48 ++++++++++++++++++++++++------------- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/backend/main.py b/backend/main.py index d5acb3a..9737720 100644 --- a/backend/main.py +++ b/backend/main.py @@ -706,16 +706,52 @@ async def paper_stats(user: dict = Depends(get_current_user)): else: sharpe = 0 - # 按币种 + # 按币种 — 完整统计 by_symbol = {} for r in rows: s = r["symbol"].replace("USDT", "") if s not in by_symbol: - by_symbol[s] = {"total": 0, "wins": 0} - by_symbol[s]["total"] += 1 - if r["pnl_r"] > 0: - by_symbol[s]["wins"] += 1 - symbol_stats = {s: {"total": v["total"], "win_rate": round(v["wins"]/v["total"]*100, 1)} for s, v in by_symbol.items()} + by_symbol[s] = [] + by_symbol[s].append(r) + + def calc_stats(trade_list): + t = len(trade_list) + w = [r for r in trade_list if r["pnl_r"] > 0] + l = [r for r in trade_list if r["pnl_r"] <= 0] + aw = sum(r["pnl_r"] for r in w) / len(w) if w else 0 + al = abs(sum(r["pnl_r"] for r in l)) / len(l) if l else 0 + wlr = aw / al if al > 0 else 0 + # MDD + pk, dd, rn = 0.0, 0.0, 0.0 + for r in sorted(trade_list, key=lambda x: x["exit_ts"] or 0): + rn += r["pnl_r"] + pk = max(pk, rn) + dd = max(dd, pk - rn) + # 夏普 + rets = [r["pnl_r"] for r in trade_list] + if len(rets) > 1: + import statistics + avg_r = statistics.mean(rets) + std_r = statistics.stdev(rets) + sp = (avg_r / std_r) * (252 ** 0.5) if std_r > 0 else 0 + else: + sp = 0 + # 方向 + lg = [r for r in trade_list if r["direction"] == "LONG"] + sh = [r for r in trade_list if r["direction"] == "SHORT"] + lwr = len([r for r in lg if r["pnl_r"] > 0]) / len(lg) * 100 if lg else 0 + swr = len([r for r in sh if r["pnl_r"] > 0]) / len(sh) * 100 if sh else 0 + total_pnl = sum(r["pnl_r"] for r in trade_list) + return { + "total": t, "win_rate": round(len(w)/t*100, 1) if t else 0, + "avg_win": round(aw, 2), "avg_loss": round(al, 2), + "win_loss_ratio": round(wlr, 2), "mdd": round(dd, 2), + "sharpe": round(sp, 2), "total_pnl": round(total_pnl, 2), + "long_win_rate": round(lwr, 1), "long_count": len(lg), + "short_win_rate": round(swr, 1), "short_count": len(sh), + } + + symbol_stats = {s: calc_stats(tl) for s, tl in by_symbol.items()} # 按方向 longs = [r for r in rows if r["direction"] == "LONG"] diff --git a/frontend/app/paper/page.tsx b/frontend/app/paper/page.tsx index a74d356..cb01b0b 100644 --- a/frontend/app/paper/page.tsx +++ b/frontend/app/paper/page.tsx @@ -379,6 +379,7 @@ function TradeHistory() { function StatsPanel() { const [data, setData] = useState(null); + const [tab, setTab] = useState("ALL"); useEffect(() => { const f = async () => { try { const r = await authFetch("/api/paper/stats"); if (r.ok) setData(await r.json()); } catch {} }; f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); @@ -386,27 +387,40 @@ function StatsPanel() { if (!data || data.error) return null; + const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"]; + const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null); + return (
-
+

详细统计

+
+ {tabs.map(t => ( + + ))} +
-
-
胜率

{data.win_rate}%

-
盈亏比

{data.win_loss_ratio}

-
平均盈利

+{data.avg_win}R

-
平均亏损

-{data.avg_loss}R

-
最大回撤

{data.mdd}R

-
夏普比率

{data.sharpe}

-
做多胜率

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

-
做空胜率

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

- {data.by_symbol && Object.entries(data.by_symbol).map(([s, v]: [string, any]) => ( -
{s}胜率

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

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

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

- ))} -
+ {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}笔)

+ ))} +
+ ) : ( +
该币种暂无数据
+ )}
); }