feat: stats panel with per-symbol tabs (ALL/BTC/ETH/XRP/SOL) - full stats for each coin
This commit is contained in:
parent
abfdc63705
commit
bda42e669a
@ -706,16 +706,52 @@ async def paper_stats(user: dict = Depends(get_current_user)):
|
|||||||
else:
|
else:
|
||||||
sharpe = 0
|
sharpe = 0
|
||||||
|
|
||||||
# 按币种
|
# 按币种 — 完整统计
|
||||||
by_symbol = {}
|
by_symbol = {}
|
||||||
for r in rows:
|
for r in rows:
|
||||||
s = r["symbol"].replace("USDT", "")
|
s = r["symbol"].replace("USDT", "")
|
||||||
if s not in by_symbol:
|
if s not in by_symbol:
|
||||||
by_symbol[s] = {"total": 0, "wins": 0}
|
by_symbol[s] = []
|
||||||
by_symbol[s]["total"] += 1
|
by_symbol[s].append(r)
|
||||||
if r["pnl_r"] > 0:
|
|
||||||
by_symbol[s]["wins"] += 1
|
def calc_stats(trade_list):
|
||||||
symbol_stats = {s: {"total": v["total"], "win_rate": round(v["wins"]/v["total"]*100, 1)} for s, v in by_symbol.items()}
|
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"]
|
longs = [r for r in rows if r["direction"] == "LONG"]
|
||||||
|
|||||||
@ -379,6 +379,7 @@ function TradeHistory() {
|
|||||||
|
|
||||||
function StatsPanel() {
|
function StatsPanel() {
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
|
const [tab, setTab] = useState("ALL");
|
||||||
useEffect(() => {
|
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 r = await authFetch("/api/paper/stats"); if (r.ok) setData(await r.json()); } catch {} };
|
||||||
f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
|
f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
|
||||||
@ -386,27 +387,40 @@ function StatsPanel() {
|
|||||||
|
|
||||||
if (!data || data.error) return null;
|
if (!data || data.error) return null;
|
||||||
|
|
||||||
|
const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"];
|
||||||
|
const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
<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>
|
<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>
|
</div>
|
||||||
<div className="p-3 grid grid-cols-2 lg:grid-cols-4 gap-2 text-xs">
|
{st ? (
|
||||||
<div><span className="text-slate-400">胜率</span><p className="font-mono font-bold">{data.win_rate}%</p></div>
|
<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_loss_ratio}</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 text-emerald-600">+{data.avg_win}R</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-red-500">-{data.avg_loss}R</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">{data.mdd}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">{data.sharpe}</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">{data.long_win_rate}% ({data.long_count}笔)</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">{data.short_win_rate}% ({data.short_count}笔)</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>
|
||||||
{data.by_symbol && Object.entries(data.by_symbol).map(([s, v]: [string, any]) => (
|
<div><span className="text-slate-400">总笔数</span><p className="font-mono font-bold">{st.total ?? data.total}</p></div>
|
||||||
<div key={s}><span className="text-slate-400">{s}胜率</span><p className="font-mono">{v.win_rate}% ({v.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>
|
||||||
{data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => (
|
{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 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>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 text-xs text-slate-400">该币种暂无数据</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user