feat: paper positions show real-time price + unrealized PnL (R + USDT)

This commit is contained in:
root 2026-02-28 11:40:11 +00:00
parent 47004ece8c
commit f90df6f3b5
2 changed files with 45 additions and 3 deletions

View File

@ -562,13 +562,46 @@ async def paper_summary(user: dict = Depends(get_current_user)):
@app.get("/api/paper/positions") @app.get("/api/paper/positions")
async def paper_positions(user: dict = Depends(get_current_user)): async def paper_positions(user: dict = Depends(get_current_user)):
"""当前活跃持仓""" """当前活跃持仓(含实时价格和浮动盈亏)"""
rows = await async_fetch( rows = await async_fetch(
"SELECT id, symbol, direction, score, tier, entry_price, entry_ts, " "SELECT id, symbol, direction, score, tier, entry_price, entry_ts, "
"tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry " "tp1_price, tp2_price, sl_price, tp1_hit, status, atr_at_entry "
"FROM paper_trades WHERE status IN ('active','tp1_hit') ORDER BY entry_ts DESC" "FROM paper_trades WHERE status IN ('active','tp1_hit') ORDER BY entry_ts DESC"
) )
return {"data": rows} # 获取实时价格
prices = {}
for r in rows:
sym = r["symbol"]
if sym not in prices:
try:
latest = await async_fetchrow(
"SELECT price FROM signal_indicators WHERE symbol=$1 ORDER BY ts DESC LIMIT 1", sym
)
prices[sym] = latest["price"] if latest else 0
except Exception:
prices[sym] = 0
result = []
for r in rows:
d = dict(r)
current_price = prices.get(r["symbol"], 0)
d["current_price"] = current_price
# 浮动盈亏(R)
entry = r["entry_price"]
atr = r["atr_at_entry"] or 1
risk_distance = 2.0 * 0.7 * atr
if risk_distance > 0 and entry > 0:
if r["direction"] == "LONG":
d["unrealized_pnl_r"] = round((current_price - entry) / risk_distance, 2)
else:
d["unrealized_pnl_r"] = round((entry - current_price) / risk_distance, 2)
# 浮动盈亏(USDT) — 假设1R = risk_per_trade
d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 200, 2) # 2% of 10000
else:
d["unrealized_pnl_r"] = 0
d["unrealized_pnl_usdt"] = 0
result.append(d)
return {"data": result}
@app.get("/api/paper/trades") @app.get("/api/paper/trades")

View File

@ -136,10 +136,19 @@ function ActivePositions() {
</span> </span>
<span className="text-[10px] text-slate-400">{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"}</span> <span className="text-[10px] text-slate-400">{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"}</span>
</div> </div>
<span className="text-[10px] text-slate-400">{holdMin}</span> <div className="flex items-center gap-2">
<span className={`font-mono text-sm font-bold ${(p.unrealized_pnl_r ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
{(p.unrealized_pnl_r ?? 0) >= 0 ? "+" : ""}{(p.unrealized_pnl_r ?? 0).toFixed(2)}R
</span>
<span className={`font-mono text-[10px] ${(p.unrealized_pnl_usdt ?? 0) >= 0 ? "text-emerald-500" : "text-red-400"}`}>
({(p.unrealized_pnl_usdt ?? 0) >= 0 ? "+" : ""}${(p.unrealized_pnl_usdt ?? 0).toFixed(0)})
</span>
<span className="text-[10px] text-slate-400">{holdMin}m</span>
</div>
</div> </div>
<div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600"> <div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600">
<span>入场: ${fmtPrice(p.entry_price)}</span> <span>入场: ${fmtPrice(p.entry_price)}</span>
<span className="text-blue-600">现价: ${p.current_price ? fmtPrice(p.current_price) : "-"}</span>
<span className="text-emerald-600">TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""}</span> <span className="text-emerald-600">TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""}</span>
<span className="text-emerald-600">TP2: ${fmtPrice(p.tp2_price)}</span> <span className="text-emerald-600">TP2: ${fmtPrice(p.tp2_price)}</span>
<span className="text-red-500">SL: ${fmtPrice(p.sl_price)}</span> <span className="text-red-500">SL: ${fmtPrice(p.sl_price)}</span>