feat: live API endpoints (/api/live/*)

- /api/live/summary: 实盘总览(含风控状态+手续费+资金费)
- /api/live/positions: 当前持仓(含binance_order_id/滑点/裸奔时间/持仓时长)
- /api/live/trades: 历史交易(含成交价/滑点/手续费/资金费)
- /api/live/equity-curve: 权益曲线
- /api/live/stats: 详细统计(含滑点P50/P95/按币种)
- /api/live/risk-status: 风控状态读取
- POST /api/live/emergency-close: 紧急全平
- POST /api/live/block-new: 禁止新开仓
- POST /api/live/resume: 恢复交易
This commit is contained in:
root 2026-03-02 09:14:05 +00:00
parent b08ea8f772
commit 832f78a1d7

View File

@ -1178,3 +1178,290 @@ async def get_server_status(user: dict = Depends(get_current_user)):
_server_cache["data"] = result
_server_cache["ts"] = now
return result
# ============================================================
# 实盘 API/api/live/...
# ============================================================
@app.get("/api/live/summary")
async def live_summary(
strategy: str = "v52_8signals",
user: dict = Depends(get_current_user),
):
"""实盘总览"""
closed = await async_fetch(
"SELECT pnl_r, direction, fee_usdt, funding_fee_usdt, slippage_bps "
"FROM live_trades WHERE status NOT IN ('active','tp1_hit') AND strategy = $1",
strategy,
)
active = await async_fetch(
"SELECT id FROM live_trades WHERE status IN ('active','tp1_hit') AND strategy = $1",
strategy,
)
first = await async_fetchrow(
"SELECT MIN(created_at) as start FROM live_trades WHERE strategy = $1",
strategy,
)
total = len(closed)
wins = len([r for r in closed if r["pnl_r"] and r["pnl_r"] > 0])
total_pnl = sum(r["pnl_r"] for r in closed if r["pnl_r"])
total_fee = sum(r["fee_usdt"] or 0 for r in closed)
total_funding = sum(r["funding_fee_usdt"] or 0 for r in closed)
win_rate = (wins / total * 100) if total > 0 else 0
gross_profit = sum(r["pnl_r"] for r in closed if r["pnl_r"] and r["pnl_r"] > 0)
gross_loss = abs(sum(r["pnl_r"] for r in closed if r["pnl_r"] and r["pnl_r"] <= 0))
profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else 0
# 读风控状态
risk_status = {}
try:
import json as _json
with open("/tmp/risk_guard_state.json") as f:
risk_status = _json.load(f)
except:
risk_status = {"status": "unknown"}
return {
"total_trades": total,
"win_rate": round(win_rate, 1),
"total_pnl_r": round(total_pnl, 2),
"total_pnl_usdt": round(total_pnl * 2, 2), # $2=1R
"active_positions": len(active),
"profit_factor": round(profit_factor, 2),
"total_fee_usdt": round(total_fee, 2),
"total_funding_usdt": round(total_funding, 2),
"start_time": str(first["start"]) if first and first["start"] else None,
"risk_status": risk_status,
}
@app.get("/api/live/positions")
async def live_positions(
strategy: str = "v52_8signals",
user: dict = Depends(get_current_user),
):
"""实盘当前持仓"""
rows = await async_fetch(
"SELECT id, symbol, direction, score, tier, strategy, entry_price, entry_ts, "
"tp1_price, tp2_price, sl_price, tp1_hit, status, risk_distance, "
"binance_order_id, fill_price, slippage_bps, protection_gap_ms, "
"signal_to_order_ms, order_to_fill_ms, score_factors "
"FROM live_trades WHERE status IN ('active','tp1_hit') AND strategy = $1 "
"ORDER BY entry_ts DESC",
strategy,
)
# 实时价格
prices = {}
symbols_needed = list(set(r["symbol"] for r in rows))
if symbols_needed:
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get("https://fapi.binance.com/fapi/v1/ticker/price")
if resp.status_code == 200:
for item in resp.json():
if item["symbol"] in symbols_needed:
prices[item["symbol"]] = float(item["price"])
except:
pass
result = []
for r in rows:
d = dict(r)
current_price = prices.get(r["symbol"], 0)
d["current_price"] = current_price
entry = r["entry_price"] or 0
rd = r.get("risk_distance") or 1
if rd > 0 and entry > 0 and current_price > 0:
if r["direction"] == "LONG":
d["unrealized_pnl_r"] = round((current_price - entry) / rd, 4)
else:
d["unrealized_pnl_r"] = round((entry - current_price) / rd, 4)
d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 2, 2)
else:
d["unrealized_pnl_r"] = 0
d["unrealized_pnl_usdt"] = 0
# 持仓时间
if r["entry_ts"]:
import time as _time
d["hold_time_min"] = round((_time.time() * 1000 - r["entry_ts"]) / 60000, 1)
result.append(d)
return {"data": result}
@app.get("/api/live/trades")
async def live_trades(
symbol: str = "all",
result: str = "all",
strategy: str = "v52_8signals",
limit: int = 100,
user: dict = Depends(get_current_user),
):
"""实盘历史交易"""
conditions = ["status NOT IN ('active','tp1_hit')"]
params = []
idx = 1
conditions.append(f"strategy = ${idx}")
params.append(strategy)
idx += 1
if symbol != "all":
conditions.append(f"symbol = ${idx}")
params.append(symbol.upper() + "USDT")
idx += 1
if result == "win":
conditions.append("pnl_r > 0")
elif result == "loss":
conditions.append("pnl_r <= 0")
where = " AND ".join(conditions)
params.append(limit)
rows = await async_fetch(
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"FROM live_trades WHERE {where} ORDER BY exit_ts DESC LIMIT ${idx}",
*params
)
return {"count": len(rows), "data": rows}
@app.get("/api/live/equity-curve")
async def live_equity_curve(
strategy: str = "v52_8signals",
user: dict = Depends(get_current_user),
):
"""实盘权益曲线"""
rows = await async_fetch(
"SELECT exit_ts, pnl_r FROM live_trades "
"WHERE status NOT IN ('active','tp1_hit') AND strategy = $1 ORDER BY exit_ts ASC",
strategy,
)
cumulative = 0.0
curve = []
for r in rows:
cumulative += r["pnl_r"] or 0
curve.append({"ts": r["exit_ts"], "pnl": round(cumulative, 2)})
return {"data": curve}
@app.get("/api/live/stats")
async def live_stats(
strategy: str = "v52_8signals",
user: dict = Depends(get_current_user),
):
"""实盘详细统计"""
rows = await async_fetch(
"SELECT symbol, direction, pnl_r, tier, entry_ts, exit_ts, slippage_bps "
"FROM live_trades WHERE status NOT IN ('active','tp1_hit') AND strategy = $1",
strategy,
)
if not rows:
return {"error": "no data"}
total = len(rows)
wins = [r for r in rows if r["pnl_r"] and r["pnl_r"] > 0]
losses = [r for r in rows if r["pnl_r"] and r["pnl_r"] <= 0]
win_rate = len(wins) / total * 100 if total > 0 else 0
avg_win = sum(r["pnl_r"] for r in wins) / len(wins) if wins else 0
avg_loss = sum(abs(r["pnl_r"]) for r in losses) / len(losses) if losses else 0
win_loss_ratio = avg_win / avg_loss if avg_loss > 0 else 0
total_pnl = sum(r["pnl_r"] for r in rows if r["pnl_r"])
# 滑点统计
slippages = [r["slippage_bps"] for r in rows if r["slippage_bps"] is not None]
avg_slippage = sum(slippages) / len(slippages) if slippages else 0
slippages_sorted = sorted(slippages) if slippages else [0]
p50_slip = slippages_sorted[len(slippages_sorted)//2] if slippages_sorted else 0
p95_idx = min(int(len(slippages_sorted)*0.95), len(slippages_sorted)-1)
p95_slip = slippages_sorted[p95_idx] if slippages_sorted else 0
# MDD
cum = 0
peak = 0
mdd = 0
for r in sorted(rows, key=lambda x: x["exit_ts"] or 0):
cum += r["pnl_r"] or 0
if cum > peak:
peak = cum
dd = peak - cum
if dd > mdd:
mdd = dd
# 按币种
by_symbol = {}
for r in rows:
s = r["symbol"]
if s not in by_symbol:
by_symbol[s] = {"wins": 0, "total": 0, "pnl": 0}
by_symbol[s]["total"] += 1
by_symbol[s]["pnl"] += r["pnl_r"] or 0
if r["pnl_r"] and r["pnl_r"] > 0:
by_symbol[s]["wins"] += 1
for s in by_symbol:
by_symbol[s]["win_rate"] = round(by_symbol[s]["wins"]/by_symbol[s]["total"]*100, 1) if by_symbol[s]["total"] > 0 else 0
by_symbol[s]["total_pnl"] = round(by_symbol[s]["pnl"], 2)
return {
"total": total,
"win_rate": round(win_rate, 1),
"avg_win": round(avg_win, 3),
"avg_loss": round(avg_loss, 3),
"win_loss_ratio": round(win_loss_ratio, 2),
"total_pnl": round(total_pnl, 2),
"mdd": round(mdd, 2),
"avg_slippage_bps": round(avg_slippage, 2),
"p50_slippage_bps": round(p50_slip, 2),
"p95_slippage_bps": round(p95_slip, 2),
"by_symbol": by_symbol,
}
@app.get("/api/live/risk-status")
async def live_risk_status(user: dict = Depends(get_current_user)):
"""风控状态"""
try:
import json as _json
with open("/tmp/risk_guard_state.json") as f:
return _json.load(f)
except:
return {"status": "unknown", "error": "risk_guard_state.json not found"}
@app.post("/api/live/emergency-close")
async def live_emergency_close(user: dict = Depends(get_current_user)):
"""紧急全平写标记文件由risk_guard执行"""
try:
import json as _json
with open("/tmp/risk_guard_emergency.json", "w") as f:
_json.dump({"action": "close_all", "time": time.time(), "user": user.get("email", "unknown")}, f)
return {"ok": True, "message": "紧急平仓指令已发送"}
except Exception as e:
return {"ok": False, "error": str(e)}
@app.post("/api/live/block-new")
async def live_block_new(user: dict = Depends(get_current_user)):
"""禁止新开仓"""
try:
import json as _json
with open("/tmp/risk_guard_emergency.json", "w") as f:
_json.dump({"action": "block_new", "time": time.time(), "user": user.get("email", "unknown")}, f)
return {"ok": True, "message": "已禁止新开仓"}
except Exception as e:
return {"ok": False, "error": str(e)}
@app.post("/api/live/resume")
async def live_resume(user: dict = Depends(get_current_user)):
"""恢复交易"""
try:
import json as _json
with open("/tmp/risk_guard_emergency.json", "w") as f:
_json.dump({"action": "resume", "time": time.time(), "user": user.get("email", "unknown")}, f)
return {"ok": True, "message": "已恢复交易"}
except Exception as e:
return {"ok": False, "error": str(e)}