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 => ())} -
| 币种 | 方向 | -入场 | 成交 | 出场 | -PnL | 状态 | -滑点 | 费用 | 时长 | +入场 | 出场 | +Gross | +Fee | +FR | +Slip | +Net | +状态 | +时长 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {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 |