feat: stats panel with per-symbol tabs (ALL/BTC/ETH/XRP/SOL) - full stats for each coin

This commit is contained in:
root 2026-03-01 01:37:02 +00:00
parent abfdc63705
commit bda42e669a
2 changed files with 73 additions and 23 deletions

View File

@ -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"]

View File

@ -379,6 +379,7 @@ function TradeHistory() {
function StatsPanel() {
const [data, setData] = useState<any>(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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100">
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between">
<h3 className="font-semibold text-slate-800 text-xs"></h3>
<div className="flex gap-1">
{tabs.map(t => (
<button key={t} onClick={() => setTab(t)}
className={`px-2 py-0.5 rounded text-[10px] font-medium transition-colors ${tab === t ? "bg-slate-800 text-white" : "bg-slate-100 text-slate-500 hover:bg-slate-200"}`}
>{t === "ALL" ? "总计" : t}</button>
))}
</div>
</div>
{st ? (
<div className="p-3 grid grid-cols-2 lg:grid-cols-4 gap-2 text-xs">
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_rate}%</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_loss_ratio}</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-emerald-600">+{data.avg_win}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-red-500">-{data.avg_loss}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.mdd}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.sharpe}</p></div>
<div><span className="text-slate-400"></span><p className="font-mono">{data.long_win_rate}% ({data.long_count})</p></div>
<div><span className="text-slate-400"></span><p className="font-mono">{data.short_win_rate}% ({data.short_count})</p></div>
{data.by_symbol && Object.entries(data.by_symbol).map(([s, v]: [string, any]) => (
<div key={s}><span className="text-slate-400">{s}</span><p className="font-mono">{v.win_rate}% ({v.total})</p></div>
))}
{data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => (
<div key={t}><span className="text-slate-400">{t === "heavy" ? "加仓" : t === "standard" ? "标准" : "轻仓"}</span><p className="font-mono">{v.win_rate}% ({v.total})</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{st.win_rate}%</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{st.win_loss_ratio}</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-emerald-600">+{st.avg_win}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-red-500">-{st.avg_loss}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{st.mdd}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{st.sharpe}</p></div>
<div><span className="text-slate-400"></span><p className={`font-mono font-bold ${(st.total_pnl ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>{(st.total_pnl ?? 0) >= 0 ? "+" : ""}{st.total_pnl ?? "-"}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{st.total ?? data.total}</p></div>
<div><span className="text-slate-400"></span><p className="font-mono">{st.long_win_rate}% ({st.long_count})</p></div>
<div><span className="text-slate-400"></span><p className="font-mono">{st.short_win_rate}% ({st.short_count})</p></div>
{tab === "ALL" && data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => (
<div key={t}><span className="text-slate-400">{t === "heavy" ? "加仓档" : t === "standard" ? "标准档" : "轻仓档"}</span><p className="font-mono">{v.win_rate}% ({v.total})</p></div>
))}
</div>
) : (
<div className="p-3 text-xs text-slate-400"></div>
)}
</div>
);
}