feat: PnL五项拆解 (gross/fee/funding/slippage/net)
后端: - /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加粗绿/红 - 每笔交易一目了然:赚了多少、扣了多少、净剩多少
This commit is contained in:
parent
ab27e5a4da
commit
fb0c3806b5
@ -1322,11 +1322,51 @@ async def live_trades(
|
|||||||
f"SELECT id, symbol, direction, score, tier, strategy, entry_price, exit_price, "
|
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"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"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}",
|
f"FROM live_trades WHERE {where} ORDER BY exit_ts DESC LIMIT ${idx}",
|
||||||
*params
|
*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")
|
@app.get("/api/live/equity-curve")
|
||||||
|
|||||||
@ -482,28 +482,39 @@ function L10_TradeHistory() {
|
|||||||
{(["all","win","loss"] as FR[]).map(r => (<button key={r} onClick={()=>setResult(r)} className={`px-2 py-0.5 rounded text-[10px] ${result===r?"bg-slate-800 text-white":"text-slate-500 hover:bg-slate-100"}`}>{r==="all"?"全部":r==="win"?"盈":"亏"}</button>))}
|
{(["all","win","loss"] as FR[]).map(r => (<button key={r} onClick={()=>setResult(r)} className={`px-2 py-0.5 rounded text-[10px] ${result===r?"bg-slate-800 text-white":"text-slate-500 hover:bg-slate-100"}`}>{r==="all"?"全部":r==="win"?"盈":"亏"}</button>))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-72 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
{trades.length === 0 ? <div className="text-center text-slate-400 text-sm py-6">暂无交易记录</div> : (
|
{trades.length === 0 ? <div className="text-center text-slate-400 text-sm py-6">暂无交易记录</div> : (
|
||||||
<table className="w-full text-[10px]">
|
<table className="w-full text-[10px]">
|
||||||
<thead className="bg-slate-50 sticky top-0"><tr className="text-slate-500">
|
<thead className="bg-slate-50 sticky top-0"><tr className="text-slate-500">
|
||||||
<th className="px-1.5 py-1 text-left">币种</th><th className="px-1.5 py-1">方向</th>
|
<th className="px-1.5 py-1 text-left">币种</th><th className="px-1.5 py-1">方向</th>
|
||||||
<th className="px-1.5 py-1 text-right">入场</th><th className="px-1.5 py-1 text-right">成交</th><th className="px-1.5 py-1 text-right">出场</th>
|
<th className="px-1.5 py-1 text-right">入场</th><th className="px-1.5 py-1 text-right">出场</th>
|
||||||
<th className="px-1.5 py-1 text-right">PnL</th><th className="px-1.5 py-1">状态</th>
|
<th className="px-1.5 py-1 text-right">Gross</th>
|
||||||
<th className="px-1.5 py-1 text-right">滑点</th><th className="px-1.5 py-1 text-right">费用</th><th className="px-1.5 py-1 text-right">时长</th>
|
<th className="px-1.5 py-1 text-right">Fee</th>
|
||||||
|
<th className="px-1.5 py-1 text-right">FR</th>
|
||||||
|
<th className="px-1.5 py-1 text-right">Slip</th>
|
||||||
|
<th className="px-1.5 py-1 text-right font-bold">Net</th>
|
||||||
|
<th className="px-1.5 py-1">状态</th>
|
||||||
|
<th className="px-1.5 py-1 text-right">时长</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody className="divide-y divide-slate-50">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{trades.map((t: any) => {
|
{trades.map((t: any) => {
|
||||||
const hm = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts-t.entry_ts)/60000) : 0;
|
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 (<tr key={t.id} className="hover:bg-slate-50">
|
return (<tr key={t.id} className="hover:bg-slate-50">
|
||||||
<td className="px-1.5 py-1 font-mono">{t.symbol?.replace("USDT","")}</td>
|
<td className="px-1.5 py-1 font-mono">{t.symbol?.replace("USDT","")}</td>
|
||||||
<td className={`px-1.5 py-1 text-center font-bold ${t.direction==="LONG"?"text-emerald-600":"text-red-500"}`}>{t.direction==="LONG"?"▲":"▼"}</td>
|
<td className={`px-1.5 py-1 text-center font-bold ${t.direction==="LONG"?"text-emerald-600":"text-red-500"}`}>{t.direction==="LONG"?"▲":"▼"}</td>
|
||||||
<td className="px-1.5 py-1 text-right font-mono">{fmtPrice(t.entry_price)}</td>
|
<td className="px-1.5 py-1 text-right font-mono">{fmtPrice(t.entry_price)}</td>
|
||||||
<td className="px-1.5 py-1 text-right font-mono">{t.fill_price?fmtPrice(t.fill_price):"-"}</td>
|
|
||||||
<td className="px-1.5 py-1 text-right font-mono">{t.exit_price?fmtPrice(t.exit_price):"-"}</td>
|
<td className="px-1.5 py-1 text-right font-mono">{t.exit_price?fmtPrice(t.exit_price):"-"}</td>
|
||||||
<td className={`px-1.5 py-1 text-right font-mono font-bold ${(t.pnl_r||0)>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</td>
|
<td className={`px-1.5 py-1 text-right font-mono ${gross>=0?"text-emerald-600":"text-red-500"}`}>{gross>=0?"+":""}{gross.toFixed(2)}</td>
|
||||||
|
<td className="px-1.5 py-1 text-right font-mono text-amber-600">-{fee.toFixed(2)}</td>
|
||||||
|
<td className="px-1.5 py-1 text-right font-mono text-violet-600">{fr > 0 ? `-${fr.toFixed(2)}` : "0"}</td>
|
||||||
|
<td className="px-1.5 py-1 text-right font-mono text-slate-500">{slip > 0 ? `-${slip.toFixed(2)}` : "0"}</td>
|
||||||
|
<td className={`px-1.5 py-1 text-right font-mono font-bold ${net>0?"text-emerald-600":net<0?"text-red-500":"text-slate-500"}`}>{net>0?"+":""}{net.toFixed(2)}R</td>
|
||||||
<td className="px-1.5 py-1 text-center"><span className={`px-1 py-0.5 rounded text-[8px] ${t.status==="tp"?"bg-emerald-100 text-emerald-700":t.status==="sl"?"bg-red-100 text-red-700":t.status==="sl_be"?"bg-amber-100 text-amber-700":"bg-slate-100 text-slate-600"}`}>{t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status}</span></td>
|
<td className="px-1.5 py-1 text-center"><span className={`px-1 py-0.5 rounded text-[8px] ${t.status==="tp"?"bg-emerald-100 text-emerald-700":t.status==="sl"?"bg-red-100 text-red-700":t.status==="sl_be"?"bg-amber-100 text-amber-700":"bg-slate-100 text-slate-600"}`}>{t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status}</span></td>
|
||||||
<td className={`px-1.5 py-1 text-right font-mono ${Math.abs(t.slippage_bps||0)>8?"text-red-500":"text-slate-600"}`}>{(t.slippage_bps||0).toFixed(1)}</td>
|
|
||||||
<td className="px-1.5 py-1 text-right font-mono text-amber-600">${(t.fee_usdt||0).toFixed(2)}</td>
|
|
||||||
<td className="px-1.5 py-1 text-right text-slate-400">{hm}m</td>
|
<td className="px-1.5 py-1 text-right text-slate-400">{hm}m</td>
|
||||||
</tr>);
|
</tr>);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user