From fb0c3806b5c61211ec731a12f479b63f57d4422b Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Mar 2026 10:13:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20PnL=E4=BA=94=E9=A1=B9=E6=8B=86=E8=A7=A3?= =?UTF-8?q?=20(gross/fee/funding/slippage/net)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - /api/live/trades 返回 gross_pnl_r, fee_r, funding_r, slippage_r, net_pnl_r - gross = 方向盈亏(含TP1半仓锁定) - fee_r = 实际手续费/risk_usd - funding_r = 不利资金费/risk_usd - slippage_r = 滑点损失估算 - net = pnl_r(已是净值) 前端L10: - 表头改为: Gross | Fee | FR | Slip | Net - 颜色: gross绿/红, fee橙, FR紫, slip灰, net加粗绿/红 - 每笔交易一目了然:赚了多少、扣了多少、净剩多少 --- backend/main.py | 44 ++++++++++++++++++++++++++++++++++++-- frontend/app/live/page.tsx | 27 ++++++++++++++++------- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/backend/main.py b/backend/main.py index 13e3529..1bd06d4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1322,11 +1322,51 @@ async def live_trades( f"SELECT id, symbol, direction, score, tier, strategy, entry_price, exit_price, " f"entry_ts, exit_ts, pnl_r, status, tp1_hit, score_factors, " f"binance_order_id, fill_price, slippage_bps, fee_usdt, funding_fee_usdt, " - f"protection_gap_ms, signal_to_order_ms, order_to_fill_ms " + f"protection_gap_ms, signal_to_order_ms, order_to_fill_ms, risk_distance, " + f"tp1_price, tp2_price, sl_price " f"FROM live_trades WHERE {where} ORDER BY exit_ts DESC LIMIT ${idx}", *params ) - return {"count": len(rows), "data": rows} + # PnL拆解 + result_data = [] + for r in rows: + d = dict(r) + entry = r["entry_price"] or 0 + exit_p = r["exit_price"] or 0 + rd = r["risk_distance"] or 1 + direction = r["direction"] + tp1_hit = r["tp1_hit"] + tp1_price = r.get("tp1_price") or 0 + + # gross_pnl_r(不含任何费用) + if direction == "LONG": + raw_r = (exit_p - entry) / rd if rd > 0 else 0 + else: + raw_r = (entry - exit_p) / rd if rd > 0 else 0 + if tp1_hit and tp1_price: + tp1_r = abs(tp1_price - entry) / rd if rd > 0 else 0 + gross_r = 0.5 * tp1_r + 0.5 * raw_r + else: + gross_r = raw_r + + fee_usdt = r["fee_usdt"] or 0 + funding_usdt = r["funding_fee_usdt"] or 0 + risk_usd = 2 # $2=1R + fee_r = fee_usdt / risk_usd if risk_usd > 0 else 0 + funding_r = abs(funding_usdt) / risk_usd if funding_usdt < 0 else 0 + # slippage_r: 滑点造成的R损失 + slippage_bps = r["slippage_bps"] or 0 + slippage_usdt = abs(slippage_bps) / 10000 * entry * (risk_usd / rd) if rd > 0 else 0 + slippage_r = slippage_usdt / risk_usd if risk_usd > 0 else 0 + + d["gross_pnl_r"] = round(gross_r, 4) + d["fee_r"] = round(fee_r, 4) + d["funding_r"] = round(funding_r, 4) + d["slippage_r"] = round(slippage_r, 4) + d["net_pnl_r"] = r["pnl_r"] # 已经是net + result_data.append(d) + + return {"count": len(result_data), "data": result_data} @app.get("/api/live/equity-curve") diff --git a/frontend/app/live/page.tsx b/frontend/app/live/page.tsx index 80d412d..b7036a0 100644 --- a/frontend/app/live/page.tsx +++ b/frontend/app/live/page.tsx @@ -482,28 +482,39 @@ function L10_TradeHistory() { {(["all","win","loss"] as FR[]).map(r => ())} -
+
{trades.length === 0 ?
暂无交易记录
: ( - - - + + + + + + + + {trades.map((t: any) => { const hm = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts-t.entry_ts)/60000) : 0; + const gross = t.gross_pnl_r || 0; + const fee = t.fee_r || 0; + const fr = t.funding_r || 0; + const slip = t.slippage_r || 0; + const net = t.net_pnl_r ?? t.pnl_r ?? 0; return ( - - + + + + + - - ); })}
币种方向入场成交出场PnL状态滑点费用时长入场出场GrossFeeFRSlipNet状态时长
{t.symbol?.replace("USDT","")} {t.direction==="LONG"?"▲":"▼"} {fmtPrice(t.entry_price)}{t.fill_price?fmtPrice(t.fill_price):"-"} {t.exit_price?fmtPrice(t.exit_price):"-"}0?"text-emerald-600":(t.pnl_r||0)<0?"text-red-500":"text-slate-500"}`}>{(t.pnl_r||0)>0?"+":""}{(t.pnl_r||0).toFixed(2)}R=0?"text-emerald-600":"text-red-500"}`}>{gross>=0?"+":""}{gross.toFixed(2)}-{fee.toFixed(2)}{fr > 0 ? `-${fr.toFixed(2)}` : "0"}{slip > 0 ? `-${slip.toFixed(2)}` : "0"}0?"text-emerald-600":net<0?"text-red-500":"text-slate-500"}`}>{net>0?"+":""}{net.toFixed(2)}R {t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status}8?"text-red-500":"text-slate-600"}`}>{(t.slippage_bps||0).toFixed(1)}${(t.fee_usdt||0).toFixed(2)} {hm}m