diff --git a/backend/main.py b/backend/main.py index 069a126..13e3529 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1465,3 +1465,340 @@ async def live_resume(user: dict = Depends(get_current_user)): return {"ok": True, "message": "已恢复交易"} except Exception as e: return {"ok": False, "error": str(e)} + + +# ============================================================ +# 实盘 API 补充(L0-L11) +# ============================================================ + +@app.get("/api/live/account") +async def live_account(user: dict = Depends(get_current_user)): + """L2: 账户概览 — 权益/保证金/杠杆/今日成交额""" + import httpx + import hashlib, hmac, time as _time + from urllib.parse import urlencode + + api_key = os.environ.get("BINANCE_API_KEY", "") + secret_key = os.environ.get("BINANCE_SECRET_KEY", "") + trade_env = os.environ.get("TRADE_ENV", "testnet") + base = "https://testnet.binancefuture.com" if trade_env == "testnet" else "https://fapi.binance.com" + + if not api_key or not secret_key: + return {"error": "API keys not configured", "equity": 0, "available_margin": 0, "used_margin": 0, "effective_leverage": 0} + + def sign(params): + params["timestamp"] = int(_time.time() * 1000) + qs = urlencode(params) + sig = hmac.new(secret_key.encode(), qs.encode(), hashlib.sha256).hexdigest() + params["signature"] = sig + return params + + try: + async with httpx.AsyncClient(timeout=5) as client: + # 账户信息 + params = sign({}) + resp = await client.get(f"{base}/fapi/v2/account", params=params, headers={"X-MBX-APIKEY": api_key}) + if resp.status_code != 200: + return {"error": f"API {resp.status_code}"} + acc = resp.json() + + equity = float(acc.get("totalWalletBalance", 0)) + available = float(acc.get("availableBalance", 0)) + used_margin = float(acc.get("totalInitialMargin", 0)) + unrealized = float(acc.get("totalUnrealizedProfit", 0)) + effective_leverage = round(used_margin / equity, 2) if equity > 0 else 0 + + # 今日已实现PnL + today_realized_r = 0 + today_fee = 0 + today_volume = 0 + try: + rows = await async_fetch( + "SELECT pnl_r, fee_usdt FROM live_trades WHERE exit_ts >= $1 AND status NOT IN ('active','tp1_hit')", + int(datetime.now(timezone.utc).replace(hour=0,minute=0,second=0,microsecond=0).timestamp() * 1000) + ) + today_realized_r = sum(r["pnl_r"] or 0 for r in rows) + today_fee = sum(r["fee_usdt"] or 0 for r in rows) + except: + pass + + return { + "equity": round(equity, 2), + "available_margin": round(available, 2), + "used_margin": round(used_margin, 2), + "unrealized_pnl": round(unrealized, 2), + "effective_leverage": effective_leverage, + "today_realized_r": round(today_realized_r, 2), + "today_realized_usdt": round(today_realized_r * 2, 2), + "today_fee": round(today_fee, 2), + "today_volume": round(today_volume, 2), + } + except Exception as e: + return {"error": str(e)} + + +@app.get("/api/live/health") +async def live_system_health(user: dict = Depends(get_current_user)): + """L11: 系统健康 — 各进程心跳、API状态、数据新鲜度""" + import subprocess, time as _time + + health = { + "ts": int(_time.time() * 1000), + "processes": {}, + "data_freshness": {}, + "api_status": "unknown", + } + + # PM2进程状态 + try: + result = subprocess.run(["pm2", "jlist"], capture_output=True, text=True, timeout=5) + import json as _json + procs = _json.loads(result.stdout) if result.stdout else [] + for p in procs: + name = p.get("name", "") + if name in ("live-executor", "position-sync", "risk-guard", "signal-engine", "market-collector", "paper-monitor", "liq-collector"): + health["processes"][name] = { + "status": p.get("pm2_env", {}).get("status", "unknown"), + "uptime_ms": p.get("pm2_env", {}).get("pm_uptime", 0), + "restarts": p.get("pm2_env", {}).get("restart_time", 0), + "memory_mb": round(p.get("monit", {}).get("memory", 0) / 1024 / 1024, 1), + "cpu": p.get("monit", {}).get("cpu", 0), + } + except: + pass + + # 数据新鲜度 + try: + now_ms = int(_time.time() * 1000) + # 最新行情数据 + latest_market = await async_fetchrow("SELECT MAX(ts) as ts FROM signal_indicators") + if latest_market and latest_market["ts"]: + age_sec = (now_ms - latest_market["ts"]) / 1000 + health["data_freshness"]["market_data"] = { + "last_ts": latest_market["ts"], + "age_sec": round(age_sec, 1), + "status": "red" if age_sec > 10 else "yellow" if age_sec > 5 else "green", + } + + # 最新对账 + risk_state = {} + try: + import json as _json2 + with open("/tmp/risk_guard_state.json") as f: + risk_state = _json2.load(f) + except: + pass + health["risk_guard"] = risk_state + except: + pass + + return health + + +@app.get("/api/live/reconciliation") +async def live_reconciliation(user: dict = Depends(get_current_user)): + """L5: 对账状态 — 本地 vs 币安""" + import httpx, hashlib, hmac, time as _time + from urllib.parse import urlencode + + api_key = os.environ.get("BINANCE_API_KEY", "") + secret_key = os.environ.get("BINANCE_SECRET_KEY", "") + trade_env = os.environ.get("TRADE_ENV", "testnet") + base = "https://testnet.binancefuture.com" if trade_env == "testnet" else "https://fapi.binance.com" + + if not api_key or not secret_key: + return {"error": "API keys not configured"} + + def sign(params): + params["timestamp"] = int(_time.time() * 1000) + qs = urlencode(params) + sig = hmac.new(secret_key.encode(), qs.encode(), hashlib.sha256).hexdigest() + params["signature"] = sig + return params + + result = {"local_positions": [], "exchange_positions": [], "local_orders": 0, "exchange_orders": 0, "diffs": [], "status": "ok"} + + try: + async with httpx.AsyncClient(timeout=5) as client: + # 币安持仓 + params = sign({}) + resp = await client.get(f"{base}/fapi/v2/positionRisk", params=params, headers={"X-MBX-APIKEY": api_key}) + exchange_positions = [] + if resp.status_code == 200: + for p in resp.json(): + amt = float(p.get("positionAmt", 0)) + if amt != 0: + exchange_positions.append({ + "symbol": p["symbol"], + "direction": "LONG" if amt > 0 else "SHORT", + "amount": abs(amt), + "entry_price": float(p.get("entryPrice", 0)), + "mark_price": float(p.get("markPrice", 0)), + "liquidation_price": float(p.get("liquidationPrice", 0)), + "unrealized_pnl": float(p.get("unRealizedProfit", 0)), + }) + result["exchange_positions"] = exchange_positions + + # 币安挂单 + params2 = sign({}) + resp2 = await client.get(f"{base}/fapi/v1/openOrders", params=params2, headers={"X-MBX-APIKEY": api_key}) + exchange_orders = resp2.json() if resp2.status_code == 200 else [] + result["exchange_orders"] = len(exchange_orders) + + # 本地持仓 + local = await async_fetch( + "SELECT id, symbol, direction, entry_price, sl_price, tp1_price, tp2_price, status, tp1_hit " + "FROM live_trades WHERE status IN ('active','tp1_hit')" + ) + result["local_positions"] = [dict(r) for r in local] + result["local_orders"] = len(local) * 3 # 预期每仓3挂单(SL+TP1+TP2) + + # 对比差异 + local_syms = {r["symbol"]: r for r in local} + exchange_syms = {p["symbol"]: p for p in exchange_positions} + + for sym, lp in local_syms.items(): + if sym not in exchange_syms: + result["diffs"].append({"symbol": sym, "type": "local_only", "severity": "critical", "detail": f"本地有{lp['direction']}仓位但币安无持仓"}) + else: + ep = exchange_syms[sym] + if lp["direction"] != ep["direction"]: + result["diffs"].append({"symbol": sym, "type": "direction_mismatch", "severity": "critical", "detail": f"本地={lp['direction']} 币安={ep['direction']}"}) + # 清算距离 + if ep["liquidation_price"] > 0 and ep["mark_price"] > 0: + if ep["direction"] == "LONG": + dist = (ep["mark_price"] - ep["liquidation_price"]) / ep["mark_price"] * 100 + else: + dist = (ep["liquidation_price"] - ep["mark_price"]) / ep["mark_price"] * 100 + if dist < 8: + result["diffs"].append({"symbol": sym, "type": "liquidation_critical", "severity": "critical", "detail": f"距清算仅{dist:.1f}%"}) + elif dist < 12: + result["diffs"].append({"symbol": sym, "type": "liquidation_warning", "severity": "high", "detail": f"距清算{dist:.1f}%"}) + + for sym, ep in exchange_syms.items(): + if sym not in local_syms: + result["diffs"].append({"symbol": sym, "type": "exchange_only", "severity": "high", "detail": f"币安有{ep['direction']}仓位但本地无记录"}) + + if result["diffs"]: + result["status"] = "mismatch" + + except Exception as e: + result["error"] = str(e) + + return result + + +@app.get("/api/live/execution-quality") +async def live_execution_quality(user: dict = Depends(get_current_user)): + """L4: 执行质量面板""" + rows = await async_fetch( + "SELECT symbol, slippage_bps, signal_to_order_ms, order_to_fill_ms, protection_gap_ms " + "FROM live_trades WHERE signal_to_order_ms IS NOT NULL ORDER BY created_at DESC LIMIT 200" + ) + if not rows: + return {"error": "no data"} + + # 按币种分组 + by_coin = {} + all_slips = [] + all_s2o = [] + all_o2f = [] + all_prot = [] + + for r in rows: + sym = r["symbol"] + if sym not in by_coin: + by_coin[sym] = {"slippages": [], "s2o": [], "o2f": [], "protection": [], "count": 0} + by_coin[sym]["count"] += 1 + if r["slippage_bps"] is not None: + by_coin[sym]["slippages"].append(r["slippage_bps"]) + all_slips.append(r["slippage_bps"]) + if r["signal_to_order_ms"] is not None: + by_coin[sym]["s2o"].append(r["signal_to_order_ms"]) + all_s2o.append(r["signal_to_order_ms"]) + if r["order_to_fill_ms"] is not None: + by_coin[sym]["o2f"].append(r["order_to_fill_ms"]) + all_o2f.append(r["order_to_fill_ms"]) + if r["protection_gap_ms"] is not None: + by_coin[sym]["protection"].append(r["protection_gap_ms"]) + all_prot.append(r["protection_gap_ms"]) + + def percentile(arr, p): + if not arr: return 0 + s = sorted(arr) + idx = min(int(len(s) * p / 100), len(s) - 1) + return s[idx] + + def stats(arr): + if not arr: return {"avg": 0, "p50": 0, "p95": 0, "min": 0, "max": 0} + return { + "avg": round(sum(arr)/len(arr), 2), + "p50": round(percentile(arr, 50), 2), + "p95": round(percentile(arr, 95), 2), + "min": round(min(arr), 2), + "max": round(max(arr), 2), + } + + result = { + "total_trades": len(rows), + "overall": { + "slippage_bps": stats(all_slips), + "signal_to_order_ms": stats(all_s2o), + "order_to_fill_ms": stats(all_o2f), + "protection_gap_ms": stats(all_prot), + }, + "by_symbol": {}, + } + + for sym, d in by_coin.items(): + result["by_symbol"][sym] = { + "count": d["count"], + "slippage_bps": stats(d["slippages"]), + "signal_to_order_ms": stats(d["s2o"]), + "order_to_fill_ms": stats(d["o2f"]), + "protection_gap_ms": stats(d["protection"]), + } + + return result + + +@app.get("/api/live/paper-comparison") +async def live_paper_comparison( + limit: int = 50, + user: dict = Depends(get_current_user), +): + """L8: 实盘 vs 模拟盘对照""" + # 按signal_id匹配 + rows = await async_fetch(""" + SELECT lt.symbol, lt.direction, lt.entry_price as live_entry, lt.exit_price as live_exit, + lt.pnl_r as live_pnl, lt.slippage_bps as live_slip, lt.entry_ts as live_entry_ts, + lt.signal_id, + pt.entry_price as paper_entry, pt.exit_price as paper_exit, pt.pnl_r as paper_pnl + FROM live_trades lt + LEFT JOIN paper_trades pt ON lt.signal_id = pt.signal_id AND lt.strategy = pt.strategy + WHERE lt.status NOT IN ('active','tp1_hit') + ORDER BY lt.exit_ts DESC + LIMIT $1 + """, limit) + + comparisons = [] + total_entry_diff = 0 + total_pnl_diff = 0 + count = 0 + + for r in rows: + d = dict(r) + if r["paper_entry"] and r["live_entry"]: + d["entry_diff"] = round(r["live_entry"] - r["paper_entry"], 6) + d["entry_diff_bps"] = round(d["entry_diff"] / r["paper_entry"] * 10000, 2) if r["paper_entry"] else 0 + if r["paper_pnl"] is not None and r["live_pnl"] is not None: + d["pnl_diff_r"] = round(r["live_pnl"] - r["paper_pnl"], 4) + total_pnl_diff += d["pnl_diff_r"] + count += 1 + comparisons.append(d) + + return { + "count": len(comparisons), + "avg_pnl_diff_r": round(total_pnl_diff / count, 4) if count > 0 else 0, + "data": comparisons, + } diff --git a/frontend/app/live/page.tsx b/frontend/app/live/page.tsx index e653372..80d412d 100644 --- a/frontend/app/live/page.tsx +++ b/frontend/app/live/page.tsx @@ -6,82 +6,103 @@ import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaCh function bjt(ms: number) { const d = new Date(ms + 8 * 3600 * 1000); - return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; -} -function fmtPrice(p: number) { - return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); + return `${String(d.getUTCMonth()+1).padStart(2,"0")}-${String(d.getUTCDate()).padStart(2,"0")} ${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`; } +function fmtPrice(p: number) { return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); } +function fmtMs(ms: number) { return ms > 999 ? `${(ms/1000).toFixed(1)}s` : `${ms}ms`; } const LIVE_STRATEGY = "v52_8signals"; -// ─── 风控状态 ──────────────────────────────────────────────────── -function RiskStatusPanel() { +// ═══════════════════════════════════════════════════════════════ +// L0: 顶部固定风险条(sticky,永远可见) +// ═══════════════════════════════════════════════════════════════ +function L0_RiskBar() { const [risk, setRisk] = useState(null); + const [recon, setRecon] = useState(null); + const [account, setAccount] = useState(null); useEffect(() => { - const f = async () => { try { const r = await authFetch("/api/live/risk-status"); if (r.ok) setRisk(await r.json()); } catch {} }; + const f = async () => { + try { const r = await authFetch("/api/live/risk-status"); if (r.ok) setRisk(await r.json()); } catch {} + try { const r = await authFetch("/api/live/reconciliation"); if (r.ok) setRecon(await r.json()); } catch {} + try { const r = await authFetch("/api/live/account"); if (r.ok) setAccount(await r.json()); } catch {} + }; f(); const iv = setInterval(f, 5000); return () => clearInterval(iv); }, []); - if (!risk) return null; - const statusColor = risk.status === "normal" ? "border-emerald-400 bg-emerald-50" : risk.status === "warning" ? "border-amber-400 bg-amber-50" : risk.status === "circuit_break" ? "border-red-400 bg-red-50" : "border-slate-200 bg-slate-50"; - const statusIcon = risk.status === "normal" ? "🟢" : risk.status === "warning" ? "🟡" : risk.status === "circuit_break" ? "🔴" : "⚪"; + + const riskStatus = risk?.status || "unknown"; + const riskColor = riskStatus === "normal" ? "bg-emerald-500" : riskStatus === "circuit_break" ? "bg-red-500" : "bg-amber-500"; + const reconOk = recon?.status === "ok"; + const totalR = (risk?.today_realized_r || 0) + Math.min(risk?.today_unrealized_r || 0, 0); + const rBudgetPct = Math.abs(totalR / 5 * 100); // -5R日限 + const rBudgetColor = rBudgetPct >= 100 ? "text-red-500" : rBudgetPct >= 80 ? "text-amber-500" : "text-emerald-500"; + return ( -
-
-
- {statusIcon} -
- 风控: {risk.status === "normal" ? "正常" : risk.status === "warning" ? "警告" : risk.status === "circuit_break" ? "熔断中" : "未知"} - {risk.circuit_break_reason &&

{risk.circuit_break_reason}

} -
+
+ {/* 交易状态 */} +
+
+ {riskStatus === "normal" ? "运行中" : riskStatus === "circuit_break" ? "🔴 熔断" : riskStatus === "warning" ? "⚠️ 警告" : "未知"} + {risk?.block_new_entries && 禁新仓} + {risk?.reduce_only && 只减仓} +
+ + {/* R预算 */} +
+
+ 已实现 + = 0 ? "text-emerald-400" : "text-red-400"}`}>{(risk?.today_realized_r||0) >= 0 ? "+" : ""}{risk?.today_realized_r||0}R
-
-
已实现

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_realized_r||0) >= 0 ? "+" : ""}{risk.today_realized_r||0}R

-
未实现

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_unrealized_r||0) >= 0 ? "+" : ""}{risk.today_unrealized_r||0}R

-
合计

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_total_r||0) >= 0 ? "+" : ""}{risk.today_total_r||0}R

-
连亏

{risk.consecutive_losses||0}次

+
+ 未实现 + = 0 ? "text-emerald-400" : "text-red-400"}`}>{(risk?.today_unrealized_r||0) >= 0 ? "+" : ""}{risk?.today_unrealized_r||0}R +
+
+ 日限 + {totalR >= 0 ? "+" : ""}{totalR.toFixed(1)}/-5R
- {(risk.block_new_entries || risk.reduce_only) && ( -
- {risk.block_new_entries && 🚫 禁止新开仓} - {risk.reduce_only && 🔒 只减仓} + + {/* 对账+清算 */} +
+
+
+ 对账{reconOk ? "✓" : `✗(${recon?.diffs?.length||0})`}
- )} +
连亏 {risk?.consecutive_losses||0}
+ {risk?.circuit_break_reason && {risk.circuit_break_reason}} +
); } -// ─── 紧急操作 ──────────────────────────────────────────────────── -function EmergencyPanel() { - const [confirming, setConfirming] = useState(null); +// ═══════════════════════════════════════════════════════════════ +// L1: 一键止血区 +// ═══════════════════════════════════════════════════════════════ +function L1_EmergencyPanel() { + const [confirming, setConfirming] = useState(null); const [msg, setMsg] = useState(""); const doAction = async (action: string) => { try { const r = await authFetch(`/api/live/${action}`, { method: "POST" }); const j = await r.json(); setMsg(j.message || j.error || "已执行"); setConfirming(null); setTimeout(() => setMsg(""), 5000); } catch { setMsg("操作失败"); } }; + const ConfirmBtn = ({ action, label, color }: { action: string; label: string; color: string }) => ( + confirming === action ? ( +
+ 确认? + + +
+ ) : ( + + ) + ); return ( -
+
-

⚡ 紧急操作

-
- {confirming === "emergency-close" ? ( -
- 确认全平? - - -
- ) : ( - - )} - {confirming === "block-new" ? ( -
- 确认? - - -
- ) : ( - - )} - +

⚡ 止血操作

+
+ + +
{msg &&

{msg}

} @@ -89,91 +110,130 @@ function EmergencyPanel() { ); } -// ─── 总览 ──────────────────────────────────────────────────────── -function SummaryCards() { +// ═══════════════════════════════════════════════════════════════ +// L2: 账户概览(8卡片) +// ═══════════════════════════════════════════════════════════════ +function L2_AccountOverview() { const [data, setData] = useState(null); + const [summary, setSummary] = useState(null); useEffect(() => { - const f = async () => { try { const r = await authFetch(`/api/live/summary?strategy=${LIVE_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} }; + const f = async () => { + try { const r = await authFetch("/api/live/account"); if (r.ok) setData(await r.json()); } catch {} + try { const r = await authFetch(`/api/live/summary?strategy=${LIVE_STRATEGY}`); if (r.ok) setSummary(await r.json()); } catch {} + }; f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); }, []); - if (!data) return
加载中...
; + const d = data || {}; + const s = summary || {}; + const cards = [ + { label: "账户权益", value: `$${(d.equity||0).toFixed(2)}`, color: "" }, + { label: "可用保证金", value: `$${(d.available_margin||0).toFixed(2)}`, color: "" }, + { label: "已用保证金", value: `$${(d.used_margin||0).toFixed(2)}`, color: "" }, + { label: "有效杠杆", value: `${d.effective_leverage||0}x`, color: (d.effective_leverage||0) > 10 ? "text-red-500" : "" }, + { label: "今日净PnL", value: `${(d.today_realized_r||0)>=0?"+":""}${d.today_realized_r||0}R ($${d.today_realized_usdt||0})`, color: (d.today_realized_r||0)>=0 ? "text-emerald-600" : "text-red-500" }, + { label: "总净PnL", value: `${(s.total_pnl_r||0)>=0?"+":""}${s.total_pnl_r||0}R ($${s.total_pnl_usdt||0})`, color: (s.total_pnl_r||0)>=0 ? "text-emerald-600" : "text-red-500" }, + { label: "成本占比", value: `$${(s.total_fee_usdt||0)+(s.total_funding_usdt||0)}`, color: "text-amber-600" }, + { label: "胜率/PF", value: `${s.win_rate||0}% / ${s.profit_factor||0}`, color: "" }, + ]; return ( -
-
-

总盈亏(R)

-

= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl_r >= 0 ? "+" : ""}{data.total_pnl_r}R

-

= 0 ? "text-emerald-500" : "text-red-400"}`}>{data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt}

-
-

胜率

{data.win_rate}%

-

总交易

{data.total_trades}

-

持仓中

{data.active_positions}

-

盈亏比(PF)

{data.profit_factor}

-

手续费

${data.total_fee_usdt}

-

资金费

${data.total_funding_usdt}

+
+ {cards.map((c, i) => ( +
+

{c.label}

+

{c.value}

+
+ ))}
); } -// ─── 当前持仓 ──────────────────────────────────────────────────── -function ActivePositions() { +// ═══════════════════════════════════════════════════════════════ +// L3: 当前持仓(WS实时) +// ═══════════════════════════════════════════════════════════════ +function L3_Positions() { const [positions, setPositions] = useState([]); - const [wsPrices, setWsPrices] = useState>({}); + const [wsPrices, setWsPrices] = useState>({}); + const [recon, setRecon] = useState(null); + useEffect(() => { - const f = async () => { try { const r = await authFetch(`/api/live/positions?strategy=${LIVE_STRATEGY}`); if (r.ok) { const j = await r.json(); setPositions(j.data || []); } } catch {} }; + const f = async () => { + try { const r = await authFetch(`/api/live/positions?strategy=${LIVE_STRATEGY}`); if (r.ok) { const j = await r.json(); setPositions(j.data||[]); } } catch {} + try { const r = await authFetch("/api/live/reconciliation"); if (r.ok) setRecon(await r.json()); } catch {} + }; f(); const iv = setInterval(f, 5000); return () => clearInterval(iv); }, []); + useEffect(() => { - const streams = ["btcusdt", "ethusdt", "xrpusdt", "solusdt"].map(s => `${s}@aggTrade`).join("/"); + const streams = ["btcusdt","ethusdt","xrpusdt","solusdt"].map(s=>`${s}@aggTrade`).join("/"); const ws = new WebSocket(`wss://fstream.binance.com/stream?streams=${streams}`); - ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.data) { setWsPrices(prev => ({ ...prev, [msg.data.s]: parseFloat(msg.data.p) })); } } catch {} }; + ws.onmessage = (e) => { try { const m = JSON.parse(e.data); if (m.data) setWsPrices(p => ({...p, [m.data.s]: parseFloat(m.data.p)})); } catch {} }; return () => ws.close(); }, []); + // 从对账数据获取清算距离 + const liqDist: Record = {}; + if (recon?.exchange_positions) { + for (const ep of recon.exchange_positions) { + if (ep.liquidation_price > 0 && ep.mark_price > 0) { + const dist = ep.direction === "LONG" + ? (ep.mark_price - ep.liquidation_price) / ep.mark_price * 100 + : (ep.liquidation_price - ep.mark_price) / ep.mark_price * 100; + liqDist[ep.symbol] = dist; + } + } + } + if (positions.length === 0) return
暂无活跃持仓
; return (
-

当前持仓 ● 实时 币安合约

+

L3 当前持仓 ● 实时

{positions.map((p: any) => { - const sym = p.symbol?.replace("USDT", "") || ""; - const holdMin = p.hold_time_min || Math.round((Date.now() - p.entry_ts) / 60000); - const currentPrice = wsPrices[p.symbol] || p.current_price || 0; + const sym = p.symbol?.replace("USDT","") || ""; + const holdMin = p.hold_time_min || Math.round((Date.now()-p.entry_ts)/60000); + const cp = wsPrices[p.symbol] || p.current_price || 0; const entry = p.entry_price || 0; const rd = p.risk_distance || 1; - const fullR = rd > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / rd : (entry - currentPrice) / rd) : 0; - const tp1R = rd > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / rd : (entry - (p.tp1_price || 0)) / rd) : 0; - const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR; + const fullR = rd > 0 ? (p.direction==="LONG"?(cp-entry)/rd:(entry-cp)/rd) : 0; + const tp1R = rd > 0 ? (p.direction==="LONG"?((p.tp1_price||0)-entry)/rd:(entry-(p.tp1_price||0))/rd) : 0; + const unrealR = p.tp1_hit ? 0.5*tp1R+0.5*fullR : fullR; const unrealUsdt = unrealR * 2; - const holdColor = holdMin >= 60 ? "text-red-500 font-bold" : holdMin >= 45 ? "text-amber-500" : "text-slate-400"; + const holdColor = holdMin>=60?"text-red-500 font-bold":holdMin>=45?"text-amber-500":"text-slate-400"; + const dist = liqDist[p.symbol]; + const distColor = dist !== undefined ? (dist < 8 ? "bg-slate-900 text-white" : dist < 12 ? "bg-red-50 text-red-700" : dist < 20 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700") : "bg-slate-50 text-slate-400"; + return (
- {p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction} - 评分{p.score} · {p.tier === "heavy" ? "加仓" : "标准"} + {p.direction==="LONG"?"🟢":"🔴"} {sym} {p.direction} + 评分{p.score} · {p.tier==="heavy"?"加仓":"标准"} + {dist !== undefined && 清算{dist.toFixed(1)}%}
- = 0 ? "text-emerald-600" : "text-red-500"}`}>{unrealR >= 0 ? "+" : ""}{unrealR.toFixed(2)}R - = 0 ? "text-emerald-500" : "text-red-400"}`}>({unrealUsdt >= 0 ? "+" : ""}${unrealUsdt.toFixed(2)}) + =0?"text-emerald-600":"text-red-500"}`}>{unrealR>=0?"+":""}{unrealR.toFixed(2)}R + =0?"text-emerald-500":"text-red-400"}`}>({unrealUsdt>=0?"+":""}${unrealUsdt.toFixed(2)}) {holdMin}m
+ {/* 价格行 */}
入场: ${fmtPrice(entry)} - 成交: ${fmtPrice(p.fill_price || entry)} - 现价: ${currentPrice ? fmtPrice(currentPrice) : "-"} - TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""} + 成交: ${fmtPrice(p.fill_price||entry)} + 现价: ${cp ? fmtPrice(cp) : "-"} + TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit?" ✅":""} TP2: ${fmtPrice(p.tp2_price)} SL: ${fmtPrice(p.sl_price)}
+ {/* 执行指标 */}
8?"bg-red-50 text-red-700":Math.abs(p.slippage_bps||0)>2.5?"bg-amber-50 text-amber-700":"bg-emerald-50 text-emerald-700"}`}>滑点 {(p.slippage_bps||0).toFixed(1)}bps - 5000?"bg-red-50 text-red-700":(p.protection_gap_ms||0)>2000?"bg-amber-50 text-amber-700":"bg-emerald-50 text-emerald-700"}`}>裸奔 {p.protection_gap_ms||0}ms - 信号→下单 {p.signal_to_order_ms||0}ms - 下单→成交 {p.order_to_fill_ms||0}ms + 5000?"bg-red-50 text-red-700":(p.protection_gap_ms||0)>2000?"bg-amber-50 text-amber-700":"bg-emerald-50 text-emerald-700"}`}>裸奔 {fmtMs(p.protection_gap_ms||0)} + S→O {fmtMs(p.signal_to_order_ms||0)} + O→F {fmtMs(p.order_to_fill_ms||0)} #{p.binance_order_id||"-"}
@@ -184,128 +244,51 @@ function ActivePositions() { ); } -// ─── 权益曲线 ──────────────────────────────────────────────────── -function EquityCurve() { - const [data, setData] = useState([]); - useEffect(() => { - const f = async () => { try { const r = await authFetch(`/api/live/equity-curve?strategy=${LIVE_STRATEGY}`); if (r.ok) { const j = await r.json(); setData(j.data || []); } } catch {} }; - f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); - }, []); - if (data.length < 2) return null; - return ( -
-

权益曲线 (累计PnL)

-
- - - bjt(v)} tick={{ fontSize: 10 }} /> - `${v}R`} /> - bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} /> - - - - -
-
- ); -} - -// ─── 历史交易 ──────────────────────────────────────────────────── -type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL"; -type FilterResult = "all" | "win" | "loss"; - -function TradeHistory() { - const [trades, setTrades] = useState([]); - const [symbol, setSymbol] = useState("all"); - const [result, setResult] = useState("all"); - useEffect(() => { - const f = async () => { try { const r = await authFetch(`/api/live/trades?symbol=${symbol}&result=${result}&strategy=${LIVE_STRATEGY}&limit=50`); if (r.ok) { const j = await r.json(); setTrades(j.data || []); } } catch {} }; - f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); - }, [symbol, result]); - - return ( -
-
-

历史交易

-
- {(["all","BTC","ETH","XRP","SOL"] as FilterSymbol[]).map(s => ())} - | - {(["all","win","loss"] as FilterResult[]).map(r => ())} -
-
-
- {trades.length === 0 ?
暂无交易记录
: ( - - - - - - - - - - - - - - - {trades.map((t: any) => { - const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0; - return ( - - - - - - - - - - - - - ); - })} - -
币种方向入场成交出场PnL(R)状态滑点费用时间
{t.symbol?.replace("USDT","")}{t.direction === "LONG" ? "🟢" : "🔴"} {t.direction}{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)}{t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status}8?"text-red-500":Math.abs(t.slippage_bps||0)>2.5?"text-amber-500":"text-slate-600"}`}>{(t.slippage_bps||0).toFixed(1)}bps${(t.fee_usdt||0).toFixed(2)}{holdMin}m
- )} -
-
- ); -} - -// ─── 详细统计 ──────────────────────────────────────────────────── -function StatsPanel() { +// ═══════════════════════════════════════════════════════════════ +// L4: 执行质量面板 +// ═══════════════════════════════════════════════════════════════ +function L4_ExecutionQuality() { const [data, setData] = useState(null); useEffect(() => { - const f = async () => { try { const r = await authFetch(`/api/live/stats?strategy=${LIVE_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} }; + const f = async () => { try { const r = await authFetch("/api/live/execution-quality"); if (r.ok) setData(await r.json()); } catch {} }; f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); }, []); if (!data || data.error) return null; + const o = data.overall || {}; + const MetricRow = ({ label, stat, unit, yellowThresh, redThresh }: any) => { + const color = stat?.p95 > redThresh ? "text-red-500" : stat?.p95 > yellowThresh ? "text-amber-500" : "text-emerald-600"; + return ( +
+ {label} +
+ avg {stat?.avg}{unit} + P50 {stat?.p50}{unit} + P95 {stat?.p95}{unit} +
+
+ ); + }; return (
-

详细统计

-
-
胜率

{data.win_rate}%

-
盈亏比

{data.win_loss_ratio}

-
平均盈利

+{data.avg_win}R

-
平均亏损

-{data.avg_loss}R

-
最大回撤

{data.mdd}R

-
总盈亏

= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl >= 0 ? "+" : ""}{data.total_pnl}R

-
总笔数

{data.total}

-
滑点P50

{data.p50_slippage_bps}bps

-
滑点P95

{data.p95_slippage_bps}bps

-
平均滑点

{data.avg_slippage_bps}bps

+

L4 执行质量 {data.total_trades}笔

+
+ + + +
- {data.by_symbol && ( + {data.by_symbol && Object.keys(data.by_symbol).length > 0 && (

按币种

-
+
{Object.entries(data.by_symbol).map(([sym, v]: [string, any]) => ( -
+
{sym.replace("USDT","")} - {v.total}笔 {v.win_rate}% - = 0 ? "text-emerald-600" : "text-red-500"}`}>{v.total_pnl >= 0 ? "+" : ""}{v.total_pnl}R + {v.count}笔 +
+ 滑点P95: {v.slippage_bps?.p95}bps + 裸奔P95: {fmtMs(v.protection_gap_ms?.p95||0)} +
))}
@@ -315,7 +298,269 @@ function StatsPanel() { ); } -// ─── 主页面 ────────────────────────────────────────────────────── +// ═══════════════════════════════════════════════════════════════ +// L5: 对账面板 +// ═══════════════════════════════════════════════════════════════ +function L5_Reconciliation() { + const [data, setData] = useState(null); + useEffect(() => { + const f = async () => { try { const r = await authFetch("/api/live/reconciliation"); if (r.ok) setData(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); + }, []); + if (!data) return null; + const ok = data.status === "ok"; + return ( +
+
+

L5 对账 {ok ? ✓ 一致 : ✗ 差异}

+
+
+
+

本地仓位 ({data.local_positions?.length || 0})

+ {(data.local_positions || []).map((p: any) => ( +
{p.symbol?.replace("USDT","")} {p.direction} @ {fmtPrice(p.entry_price)}
+ ))} + {!data.local_positions?.length &&
} +
+
+

币安仓位 ({data.exchange_positions?.length || 0})

+ {(data.exchange_positions || []).map((p: any, i: number) => ( +
{p.symbol?.replace("USDT","")} {p.direction} qty={p.amount} liq={fmtPrice(p.liquidation_price)}
+ ))} + {!data.exchange_positions?.length &&
} +
+
+
+ 挂单: 本地预期 {data.local_orders||0} / 币安 {data.exchange_orders||0} +
+ {data.diffs?.length > 0 && ( +
+ {data.diffs.map((d: any, i: number) => ( +
+ ⚠ [{d.symbol}] {d.detail} +
+ ))} +
+ )} +
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// L6: 风控状态 +// ═══════════════════════════════════════════════════════════════ +function L6_RiskStatus() { + const [risk, setRisk] = useState(null); + useEffect(() => { + const f = async () => { try { const r = await authFetch("/api/live/risk-status"); if (r.ok) setRisk(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 5000); return () => clearInterval(iv); + }, []); + if (!risk) return null; + const thresholds = [ + { rule: "单日亏损 > -5R", status: (risk.today_total_r||0) > -5 ? "✅" : "🔴" }, + { rule: "连续亏损 < 5次", status: (risk.consecutive_losses||0) < 5 ? "✅" : "🔴" }, + { rule: "API连接正常", status: risk.status !== "circuit_break" || !risk.circuit_break_reason?.includes("API") ? "✅" : "🔴" }, + ]; + return ( +
+

L6 风控状态

+
+
+ {thresholds.map((t, i) => ( +
{t.status}{t.rule}
+ ))} +
+ {risk.circuit_break_reason && ( +
+ 熔断原因:{risk.circuit_break_reason} + {risk.auto_resume_time && 预计恢复: {new Date(risk.auto_resume_time * 1000).toLocaleTimeString("zh-CN")}} +
+ )} +
+
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// L8: 实盘 vs 模拟盘对照 +// ═══════════════════════════════════════════════════════════════ +function L8_PaperComparison() { + const [data, setData] = useState(null); + useEffect(() => { + const f = async () => { try { const r = await authFetch("/api/live/paper-comparison?limit=20"); if (r.ok) setData(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); + }, []); + if (!data || !data.data?.length) return null; + return ( +
+
+

L8 实盘 vs 模拟盘 平均R差: {data.avg_pnl_diff_r}R

+
+
+ + + + + + + + {data.data.map((r: any, i: number) => ( + + + + + + + + + + + ))} + +
币种方向实盘入场模拟入场价差bps实盘PnL模拟PnLR差
{r.symbol?.replace("USDT","")}{r.direction}{r.live_entry ? fmtPrice(r.live_entry) : "-"}{r.paper_entry ? fmtPrice(r.paper_entry) : "-"}{r.entry_diff_bps || "-"}=0?"text-emerald-600":"text-red-500"}`}>{r.live_pnl?.toFixed(2) || "-"}=0?"text-emerald-600":"text-red-500"}`}>{r.paper_pnl?.toFixed(2) || "-"}=0?"text-emerald-600":"text-red-500"}`}>{r.pnl_diff_r?.toFixed(2) || "-"}
+
+
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// L9: 权益曲线+回撤 +// ═══════════════════════════════════════════════════════════════ +function L9_EquityCurve() { + const [data, setData] = useState([]); + useEffect(() => { + const f = async () => { try { const r = await authFetch(`/api/live/equity-curve?strategy=${LIVE_STRATEGY}`); if (r.ok) { const j = await r.json(); setData(j.data||[]); } } catch {} }; + f(); const iv = setInterval(f, 30000); return () => clearInterval(iv); + }, []); + if (data.length < 2) return null; + // 计算回撤 + let peak = 0; + const withDD = data.map(d => { + if (d.pnl > peak) peak = d.pnl; + return { ...d, dd: -(peak - d.pnl) }; + }); + return ( +
+

L9 权益曲线 + 回撤

+
+ + + bjt(v)} tick={{ fontSize: 10 }} /> + `${v}R`} /> + bjt(Number(v))} /> + + + + + +
+
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// L10: 历史交易表 +// ═══════════════════════════════════════════════════════════════ +type FS = "all"|"BTC"|"ETH"|"XRP"|"SOL"; +type FR = "all"|"win"|"loss"; + +function L10_TradeHistory() { + const [trades, setTrades] = useState([]); + const [symbol, setSymbol] = useState("all"); + const [result, setResult] = useState("all"); + useEffect(() => { + const f = async () => { try { const r = await authFetch(`/api/live/trades?symbol=${symbol}&result=${result}&strategy=${LIVE_STRATEGY}&limit=50`); if (r.ok) { const j = await r.json(); setTrades(j.data||[]); } } catch {} }; + f(); const iv = setInterval(f, 15000); return () => clearInterval(iv); + }, [symbol, result]); + return ( +
+
+

L10 历史交易

+
+ {(["all","BTC","ETH","XRP","SOL"] as FS[]).map(s => ())} + | + {(["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; + return ( + + + + + + + + + + + ); + })} + +
币种方向入场成交出场PnL状态滑点费用时长
{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{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
+ )} +
+
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// L11: 系统健康 +// ═══════════════════════════════════════════════════════════════ +function L11_SystemHealth() { + const [data, setData] = useState(null); + useEffect(() => { + const f = async () => { try { const r = await authFetch("/api/live/health"); if (r.ok) setData(await r.json()); } catch {} }; + f(); const iv = setInterval(f, 10000); return () => clearInterval(iv); + }, []); + if (!data) return null; + const procs = data.processes || {}; + const fresh = data.data_freshness || {}; + return ( +
+

L11 系统健康

+
+ {Object.keys(procs).length > 0 && ( +
+

进程状态

+
+ {Object.entries(procs).map(([name, p]: [string, any]) => ( +
+ {name} + {p.status} + {p.memory_mb}MB ↻{p.restarts} +
+ ))} +
+
+ )} + {fresh.market_data && ( +
+ 行情数据: + + {fresh.market_data.age_sec}秒前 {fresh.market_data.status==="green"?"✓":"⚠"} + +
+ )} +
+
+ ); +} + +// ═══════════════════════════════════════════════════════════════ +// 主页面 +// ═══════════════════════════════════════════════════════════════ export default function LiveTradingPage() { const { isLoggedIn, loading } = useAuth(); if (loading) return
加载中...
; @@ -326,20 +571,25 @@ export default function LiveTradingPage() { 登录
); - return (
-
-

⚡ 实盘交易

-

V5.2策略 · 币安USDT永续合约 · 测试网

+ +
+
+

⚡ 实盘交易

+

V5.2策略 · 币安USDT永续合约 · 测试网

+
- - - - - - - + + + + + + + + + +
); } \ No newline at end of file