feat: 完整11层实盘页面 + 补充API

前端(/live page.tsx 594行):
- L0: 顶部固定风险条(sticky) - 交易状态/R预算/对账/清算/连亏
- L1: 一键止血区 - 全平/禁新仓/恢复(双重确认)
- L2: 账户概览8卡片 - 权益/保证金/杠杆/今日PnL/总PnL/成本/胜率PF
- L3: 当前持仓(WS实时) - 含清算距离/滑点/裸奔/延迟/OrderID
- L4: 执行质量面板 - 滑点/延迟P50/P95按币种分组
- L5: 对账面板 - 本地vs币安持仓+挂单+差异列表
- L6: 风控状态 - 规则检查+熔断原因+恢复条件
- L8: 实盘vs模拟盘对照 - signal_id匹配+入场差/R差
- L9: 权益曲线+回撤 - 双Area叠加
- L10: 历史交易 - 含成交价/滑点/费用+筛选
- L11: 系统健康 - PM2进程状态+数据新鲜度

后端新增API:
- /api/live/account: 币安账户数据
- /api/live/health: PM2进程+数据新鲜度
- /api/live/reconciliation: 对账(本地vs币安)
- /api/live/execution-quality: 执行质量统计
- /api/live/paper-comparison: 实盘vs模拟盘
This commit is contained in:
root 2026-03-02 09:38:14 +00:00
parent 1ef1f97b5d
commit cb869926e2
2 changed files with 802 additions and 215 deletions

View File

@ -1465,3 +1465,340 @@ async def live_resume(user: dict = Depends(get_current_user)):
return {"ok": True, "message": "已恢复交易"} return {"ok": True, "message": "已恢复交易"}
except Exception as e: except Exception as e:
return {"ok": False, "error": str(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,
}

View File

@ -8,80 +8,101 @@ function bjt(ms: number) {
const d = new Date(ms + 8 * 3600 * 1000); 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")}`; 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) { function fmtPrice(p: number) { return p < 100 ? p.toFixed(4) : p.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); }
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"; const LIVE_STRATEGY = "v52_8signals";
// ─── 风控状态 ──────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
function RiskStatusPanel() { // L0: 顶部固定风险条sticky永远可见
// ═══════════════════════════════════════════════════════════════
function L0_RiskBar() {
const [risk, setRisk] = useState<any>(null); const [risk, setRisk] = useState<any>(null);
const [recon, setRecon] = useState<any>(null);
const [account, setAccount] = useState<any>(null);
useEffect(() => { 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); 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 riskStatus = risk?.status || "unknown";
const statusIcon = risk.status === "normal" ? "🟢" : risk.status === "warning" ? "🟡" : risk.status === "circuit_break" ? "🔴" : "⚪"; 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 ( return (
<div className={`rounded-xl border-2 ${statusColor} px-4 py-3`}> <div className="sticky top-0 z-50 bg-slate-900 text-white px-4 py-2 rounded-lg shadow-lg flex items-center justify-between flex-wrap gap-2 text-[11px]">
<div className="flex items-center justify-between flex-wrap gap-2"> {/* 交易状态 */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-1.5">
<span className="text-lg">{statusIcon}</span> <div className={`w-2 h-2 rounded-full ${riskColor} ${riskStatus === "circuit_break" ? "animate-pulse" : ""}`} />
<span className="font-medium">{riskStatus === "normal" ? "运行中" : riskStatus === "circuit_break" ? "🔴 熔断" : riskStatus === "warning" ? "⚠️ 警告" : "未知"}</span>
{risk?.block_new_entries && <span className="px-1 py-0.5 rounded bg-red-800 text-red-200 text-[9px]"></span>}
{risk?.reduce_only && <span className="px-1 py-0.5 rounded bg-red-800 text-red-200 text-[9px]"></span>}
</div>
{/* R预算 */}
<div className="flex items-center gap-3 font-mono">
<div> <div>
<span className="font-bold text-sm text-slate-800">: {risk.status === "normal" ? "正常" : risk.status === "warning" ? "警告" : risk.status === "circuit_break" ? "熔断中" : "未知"}</span> <span className="text-slate-400"></span>
{risk.circuit_break_reason && <p className="text-[10px] text-red-600 mt-0.5">{risk.circuit_break_reason}</p>} <span className={`ml-1 font-bold ${(risk?.today_realized_r||0) >= 0 ? "text-emerald-400" : "text-red-400"}`}>{(risk?.today_realized_r||0) >= 0 ? "+" : ""}{risk?.today_realized_r||0}R</span>
</div>
<div>
<span className="text-slate-400"></span>
<span className={`ml-1 font-bold ${(risk?.today_unrealized_r||0) >= 0 ? "text-emerald-400" : "text-red-400"}`}>{(risk?.today_unrealized_r||0) >= 0 ? "+" : ""}{risk?.today_unrealized_r||0}R</span>
</div>
<div>
<span className="text-slate-400"></span>
<span className={`ml-1 font-bold ${rBudgetColor}`}>{totalR >= 0 ? "+" : ""}{totalR.toFixed(1)}/-5R</span>
</div> </div>
</div> </div>
<div className="flex gap-4 text-[11px] font-mono">
<div><span className="text-slate-400"></span><p className={`font-bold ${(risk.today_realized_r||0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_realized_r||0) >= 0 ? "+" : ""}{risk.today_realized_r||0}R</p></div> {/* 对账+清算 */}
<div><span className="text-slate-400"></span><p className={`font-bold ${(risk.today_unrealized_r||0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_unrealized_r||0) >= 0 ? "+" : ""}{risk.today_unrealized_r||0}R</p></div> <div className="flex items-center gap-3">
<div><span className="text-slate-400"></span><p className={`font-bold ${(risk.today_total_r||0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_total_r||0) >= 0 ? "+" : ""}{risk.today_total_r||0}R</p></div> <div className="flex items-center gap-1">
<div><span className="text-slate-400"></span><p className="font-bold text-slate-800">{risk.consecutive_losses||0}</p></div> <div className={`w-1.5 h-1.5 rounded-full ${reconOk ? "bg-emerald-400" : "bg-red-400 animate-pulse"}`} />
<span className="text-slate-300">{reconOk ? "✓" : `✗(${recon?.diffs?.length||0})`}</span>
</div> </div>
<div className="text-slate-300"> <span className="font-bold text-white">{risk?.consecutive_losses||0}</span></div>
{risk?.circuit_break_reason && <span className="text-red-300 text-[9px] max-w-[150px] truncate">{risk.circuit_break_reason}</span>}
</div> </div>
{(risk.block_new_entries || risk.reduce_only) && (
<div className="mt-2 flex gap-2">
{risk.block_new_entries && <span className="text-[10px] px-2 py-0.5 rounded bg-red-100 text-red-700 font-medium">🚫 </span>}
{risk.reduce_only && <span className="text-[10px] px-2 py-0.5 rounded bg-red-100 text-red-700 font-medium">🔒 </span>}
</div>
)}
</div> </div>
); );
} }
// ─── 紧急操作 ──────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
function EmergencyPanel() { // L1: 一键止血区
// ═══════════════════════════════════════════════════════════════
function L1_EmergencyPanel() {
const [confirming, setConfirming] = useState<string|null>(null); const [confirming, setConfirming] = useState<string|null>(null);
const [msg, setMsg] = useState(""); const [msg, setMsg] = useState("");
const doAction = async (action: string) => { 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("操作失败"); } 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 ? (
<div className="flex items-center gap-1">
<span className="text-[10px] text-red-600"></span>
<button onClick={() => doAction(action)} className={`px-2 py-1 rounded text-[10px] font-bold ${color} text-white`}></button>
<button onClick={() => setConfirming(null)} className="px-2 py-1 rounded text-[10px] bg-slate-200 text-slate-600"></button>
</div>
) : (
<button onClick={() => setConfirming(action)} className={`px-3 py-1.5 rounded-lg text-[11px] font-bold ${color} text-white`}>{label}</button>
)
);
return ( return (
<div className="rounded-xl border border-slate-200 bg-white px-4 py-3"> <div className="rounded-xl border border-slate-200 bg-white px-4 py-2.5">
<div className="flex items-center justify-between flex-wrap gap-2"> <div className="flex items-center justify-between flex-wrap gap-2">
<h3 className="font-semibold text-slate-800 text-xs"> </h3> <h3 className="font-semibold text-slate-800 text-xs"> </h3>
<div className="flex gap-2"> <div className="flex gap-2 items-center">
{confirming === "emergency-close" ? ( <ConfirmBtn action="emergency-close" label="🔴 全平" color="bg-red-500 hover:bg-red-600" />
<div className="flex items-center gap-1"> <ConfirmBtn action="block-new" label="🟡 禁新仓" color="bg-amber-500 hover:bg-amber-600" />
<span className="text-[10px] text-red-600 font-medium"></span> <button onClick={() => doAction("resume")} className="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-emerald-500 text-white hover:bg-emerald-600"> </button>
<button onClick={() => doAction("emergency-close")} className="px-2 py-1 rounded text-[10px] font-bold bg-red-600 text-white"></button>
<button onClick={() => setConfirming(null)} className="px-2 py-1 rounded text-[10px] bg-slate-200 text-slate-600"></button>
</div>
) : (
<button onClick={() => setConfirming("emergency-close")} className="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-red-500 text-white hover:bg-red-600">🔴 </button>
)}
{confirming === "block-new" ? (
<div className="flex items-center gap-1">
<span className="text-[10px] text-amber-600 font-medium"></span>
<button onClick={() => doAction("block-new")} className="px-2 py-1 rounded text-[10px] font-bold bg-amber-500 text-white"></button>
<button onClick={() => setConfirming(null)} className="px-2 py-1 rounded text-[10px] bg-slate-200 text-slate-600"></button>
</div>
) : (
<button onClick={() => setConfirming("block-new")} className="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-amber-500 text-white hover:bg-amber-600">🟡 </button>
)}
<button onClick={() => doAction("resume")} className="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-emerald-500 text-white hover:bg-emerald-600"> </button>
</div> </div>
</div> </div>
{msg && <p className="text-[10px] text-blue-600 mt-1">{msg}</p>} {msg && <p className="text-[10px] text-blue-600 mt-1">{msg}</p>}
@ -89,71 +110,108 @@ function EmergencyPanel() {
); );
} }
// ─── 总览 ──────────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
function SummaryCards() { // L2: 账户概览8卡片
// ═══════════════════════════════════════════════════════════════
function L2_AccountOverview() {
const [data, setData] = useState<any>(null); const [data, setData] = useState<any>(null);
const [summary, setSummary] = useState<any>(null);
useEffect(() => { 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); f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
}, []); }, []);
if (!data) return <div className="text-center text-slate-400 text-sm py-4">...</div>; 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 ( return (
<div className="grid grid-cols-3 lg:grid-cols-7 gap-1.5"> <div className="grid grid-cols-4 lg:grid-cols-8 gap-1.5">
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"> {cards.map((c, i) => (
<p className="text-[10px] text-slate-400">(R)</p> <div key={i} className="bg-white rounded-lg border border-slate-200 px-2 py-1.5">
<p className={`font-mono font-bold text-lg ${data.total_pnl_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl_r >= 0 ? "+" : ""}{data.total_pnl_r}R</p> <p className="text-[9px] text-slate-400 truncate">{c.label}</p>
<p className={`font-mono text-[10px] ${data.total_pnl_usdt >= 0 ? "text-emerald-500" : "text-red-400"}`}>{data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt}</p> <p className={`font-mono font-bold text-sm ${c.color || "text-slate-800"} truncate`}>{c.value}</p>
</div> </div>
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400"></p><p className="font-mono font-bold text-lg text-slate-800">{data.win_rate}%</p></div> ))}
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400"></p><p className="font-mono font-bold text-lg text-slate-800">{data.total_trades}</p></div>
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400"></p><p className="font-mono font-bold text-lg text-blue-600">{data.active_positions}</p></div>
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400">(PF)</p><p className="font-mono font-bold text-lg text-slate-800">{data.profit_factor}</p></div>
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400"></p><p className="font-mono font-bold text-sm text-amber-600">${data.total_fee_usdt}</p></div>
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2"><p className="text-[10px] text-slate-400"></p><p className="font-mono font-bold text-sm text-violet-600">${data.total_funding_usdt}</p></div>
</div> </div>
); );
} }
// ─── 当前持仓 ──────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
function ActivePositions() { // L3: 当前持仓WS实时
// ═══════════════════════════════════════════════════════════════
function L3_Positions() {
const [positions, setPositions] = useState<any[]>([]); const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string,number>>({}); const [wsPrices, setWsPrices] = useState<Record<string,number>>({});
const [recon, setRecon] = useState<any>(null);
useEffect(() => { 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); f(); const iv = setInterval(f, 5000); return () => clearInterval(iv);
}, []); }, []);
useEffect(() => { 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}`); 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(); return () => ws.close();
}, []); }, []);
// 从对账数据获取清算距离
const liqDist: Record<string, number> = {};
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 <div className="rounded-xl border border-slate-200 bg-white px-3 py-4 text-center text-slate-400 text-sm"></div>; if (positions.length === 0) return <div className="rounded-xl border border-slate-200 bg-white px-3 py-4 text-center text-slate-400 text-sm"></div>;
return ( return (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden"> <div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"> <div className="px-3 py-2 border-b border-slate-100">
<h3 className="font-semibold text-slate-800 text-xs"> <span className="text-[10px] text-emerald-500 font-normal"> </span> <span className="text-[10px] text-slate-400 font-normal ml-2"></span></h3> <h3 className="font-semibold text-slate-800 text-xs">L3 <span className="text-[10px] text-emerald-500"> </span></h3>
</div> </div>
<div className="divide-y divide-slate-100"> <div className="divide-y divide-slate-100">
{positions.map((p: any) => { {positions.map((p: any) => {
const sym = p.symbol?.replace("USDT","") || ""; const sym = p.symbol?.replace("USDT","") || "";
const holdMin = p.hold_time_min || Math.round((Date.now()-p.entry_ts)/60000); const holdMin = p.hold_time_min || Math.round((Date.now()-p.entry_ts)/60000);
const currentPrice = wsPrices[p.symbol] || p.current_price || 0; const cp = wsPrices[p.symbol] || p.current_price || 0;
const entry = p.entry_price || 0; const entry = p.entry_price || 0;
const rd = p.risk_distance || 1; const rd = p.risk_distance || 1;
const fullR = rd > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / rd : (entry - currentPrice) / rd) : 0; 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 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 unrealR = p.tp1_hit ? 0.5*tp1R+0.5*fullR : fullR;
const unrealUsdt = unrealR * 2; 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 ( return (
<div key={p.id} className="px-3 py-2"> <div key={p.id} className="px-3 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`text-xs font-bold ${p.direction==="LONG"?"text-emerald-600":"text-red-500"}`}>{p.direction==="LONG"?"🟢":"🔴"} {sym} {p.direction}</span> <span className={`text-xs font-bold ${p.direction==="LONG"?"text-emerald-600":"text-red-500"}`}>{p.direction==="LONG"?"🟢":"🔴"} {sym} {p.direction}</span>
<span className="text-[10px] text-slate-400">{p.score} · {p.tier==="heavy"?"加仓":"标准"}</span> <span className="text-[10px] text-slate-400">{p.score} · {p.tier==="heavy"?"加仓":"标准"}</span>
{dist !== undefined && <span className={`text-[9px] px-1.5 py-0.5 rounded font-mono font-bold ${distColor}`}>{dist.toFixed(1)}%</span>}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`font-mono text-sm font-bold ${unrealR>=0?"text-emerald-600":"text-red-500"}`}>{unrealR>=0?"+":""}{unrealR.toFixed(2)}R</span> <span className={`font-mono text-sm font-bold ${unrealR>=0?"text-emerald-600":"text-red-500"}`}>{unrealR>=0?"+":""}{unrealR.toFixed(2)}R</span>
@ -161,19 +219,21 @@ function ActivePositions() {
<span className={`text-[10px] ${holdColor}`}>{holdMin}m</span> <span className={`text-[10px] ${holdColor}`}>{holdMin}m</span>
</div> </div>
</div> </div>
{/* 价格行 */}
<div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600 flex-wrap"> <div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600 flex-wrap">
<span>入场: ${fmtPrice(entry)}</span> <span>入场: ${fmtPrice(entry)}</span>
<span>成交: ${fmtPrice(p.fill_price||entry)}</span> <span>成交: ${fmtPrice(p.fill_price||entry)}</span>
<span className="text-blue-600">现价: ${currentPrice ? fmtPrice(currentPrice) : "-"}</span> <span className="text-blue-600">现价: ${cp ? fmtPrice(cp) : "-"}</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>
</div> </div>
{/* 执行指标 */}
<div className="flex gap-2 mt-1 flex-wrap"> <div className="flex gap-2 mt-1 flex-wrap">
<span className={`text-[9px] px-1.5 py-0.5 rounded ${Math.abs(p.slippage_bps||0)>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</span> <span className={`text-[9px] px-1.5 py-0.5 rounded ${Math.abs(p.slippage_bps||0)>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</span>
<span className={`text-[9px] px-1.5 py-0.5 rounded ${(p.protection_gap_ms||0)>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</span> <span className={`text-[9px] px-1.5 py-0.5 rounded ${(p.protection_gap_ms||0)>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)}</span>
<span className="text-[9px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-700"> {p.signal_to_order_ms||0}ms</span> <span className="text-[9px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-700">SO {fmtMs(p.signal_to_order_ms||0)}</span>
<span className="text-[9px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-700"> {p.order_to_fill_ms||0}ms</span> <span className="text-[9px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-700">OF {fmtMs(p.order_to_fill_ms||0)}</span>
<span className="text-[9px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600">#{p.binance_order_id||"-"}</span> <span className="text-[9px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600">#{p.binance_order_id||"-"}</span>
</div> </div>
</div> </div>
@ -184,128 +244,51 @@ function ActivePositions() {
); );
} }
// ─── 权益曲线 ──────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
function EquityCurve() { // L4: 执行质量面板
const [data, setData] = useState<any[]>([]); // ═══════════════════════════════════════════════════════════════
useEffect(() => { function L4_ExecutionQuality() {
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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">线 (PnL)</h3></div>
<div className="p-2" style={{ height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<XAxis dataKey="ts" tickFormatter={(v) => bjt(v)} tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => `${v}R`} />
<Tooltip labelFormatter={(v) => bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} />
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="3 3" />
<Area type="monotone" dataKey="pnl" stroke="#10b981" fill="#d1fae5" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
}
// ─── 历史交易 ────────────────────────────────────────────────────
type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL";
type FilterResult = "all" | "win" | "loss";
function TradeHistory() {
const [trades, setTrades] = useState<any[]>([]);
const [symbol, setSymbol] = useState<FilterSymbol>("all");
const [result, setResult] = useState<FilterResult>("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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
<h3 className="font-semibold text-slate-800 text-xs"></h3>
<div className="flex gap-1">
{(["all","BTC","ETH","XRP","SOL"] as FilterSymbol[]).map(s => (<button key={s} onClick={() => setSymbol(s)} className={`px-2 py-0.5 rounded text-[10px] ${symbol === s ? "bg-slate-800 text-white" : "text-slate-500 hover:bg-slate-100"}`}>{s === "all" ? "全部" : s}</button>))}
<span className="text-slate-300">|</span>
{(["all","win","loss"] as FilterResult[]).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 className="max-h-72 overflow-y-auto">
{trades.length === 0 ? <div className="text-center text-slate-400 text-sm py-6"></div> : (
<table className="w-full text-[11px]">
<thead className="bg-slate-50 sticky top-0"><tr className="text-slate-500">
<th className="px-2 py-1.5 text-left font-medium"></th>
<th className="px-2 py-1.5 text-left font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium">PnL(R)</th>
<th className="px-2 py-1.5 text-center font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
<th className="px-2 py-1.5 text-right font-medium"></th>
</tr></thead>
<tbody className="divide-y divide-slate-50">
{trades.map((t: any) => {
const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0;
return (
<tr key={t.id} className="hover:bg-slate-50">
<td className="px-2 py-1.5 font-mono">{t.symbol?.replace("USDT","")}</td>
<td className={`px-2 py-1.5 font-bold ${t.direction === "LONG" ? "text-emerald-600" : "text-red-500"}`}>{t.direction === "LONG" ? "🟢" : "🔴"} {t.direction}</td>
<td className="px-2 py-1.5 text-right font-mono">{fmtPrice(t.entry_price)}</td>
<td className="px-2 py-1.5 text-right font-mono">{t.fill_price ? fmtPrice(t.fill_price) : "-"}</td>
<td className="px-2 py-1.5 text-right font-mono">{t.exit_price ? fmtPrice(t.exit_price) : "-"}</td>
<td className={`px-2 py-1.5 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)}</td>
<td className="px-2 py-1.5 text-center"><span className={`px-1 py-0.5 rounded text-[9px] ${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-2 py-1.5 text-right font-mono ${Math.abs(t.slippage_bps||0)>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</td>
<td className="px-2 py-1.5 text-right font-mono text-amber-600">${(t.fee_usdt||0).toFixed(2)}</td>
<td className="px-2 py-1.5 text-right text-slate-400">{holdMin}m</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
);
}
// ─── 详细统计 ────────────────────────────────────────────────────
function StatsPanel() {
const [data, setData] = useState<any>(null); const [data, setData] = useState<any>(null);
useEffect(() => { 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); f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
}, []); }, []);
if (!data || data.error) return null; 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 (
<div className="flex items-center justify-between text-[11px]">
<span className="text-slate-500">{label}</span>
<div className="flex gap-3 font-mono">
<span className="text-slate-400">avg <span className="text-slate-800 font-bold">{stat?.avg}{unit}</span></span>
<span className="text-slate-400">P50 <span className="text-slate-800">{stat?.p50}{unit}</span></span>
<span className="text-slate-400">P95 <span className={`font-bold ${color}`}>{stat?.p95}{unit}</span></span>
</div>
</div>
);
};
return ( return (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden"> <div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs"></h3></div> <div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">L4 <span className="text-[10px] text-slate-400">{data.total_trades}</span></h3></div>
<div className="p-3 grid grid-cols-2 lg:grid-cols-4 gap-2 text-xs"> <div className="p-3 space-y-2">
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_rate}%</p></div> <MetricRow label="滑点" stat={o.slippage_bps} unit="bps" yellowThresh={2.5} redThresh={8} />
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_loss_ratio}</p></div> <MetricRow label="信号→下单" stat={o.signal_to_order_ms} unit="ms" yellowThresh={250} redThresh={1200} />
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-emerald-600">+{data.avg_win}R</p></div> <MetricRow label="下单→成交" stat={o.order_to_fill_ms} unit="ms" yellowThresh={600} redThresh={3500} />
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-red-500">-{data.avg_loss}R</p></div> <MetricRow label="裸奔时间" stat={o.protection_gap_ms} unit="ms" yellowThresh={2000} redThresh={5000} />
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.mdd}R</p></div>
<div><span className="text-slate-400"></span><p className={`font-mono font-bold ${data.total_pnl >= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl >= 0 ? "+" : ""}{data.total_pnl}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.total}</p></div>
<div><span className="text-slate-400">P50</span><p className="font-mono font-bold">{data.p50_slippage_bps}bps</p></div>
<div><span className="text-slate-400">P95</span><p className="font-mono font-bold">{data.p95_slippage_bps}bps</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.avg_slippage_bps}bps</p></div>
</div> </div>
{data.by_symbol && ( {data.by_symbol && Object.keys(data.by_symbol).length > 0 && (
<div className="px-3 pb-3"> <div className="px-3 pb-3">
<p className="text-[10px] text-slate-400 mb-1"></p> <p className="text-[10px] text-slate-400 mb-1"></p>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-1.5">
{Object.entries(data.by_symbol).map(([sym, v]: [string, any]) => ( {Object.entries(data.by_symbol).map(([sym, v]: [string, any]) => (
<div key={sym} className="rounded-lg bg-slate-50 px-2 py-1.5 text-[11px]"> <div key={sym} className="rounded-lg bg-slate-50 px-2 py-1.5 text-[10px]">
<span className="font-mono font-bold">{sym.replace("USDT","")}</span> <span className="font-mono font-bold">{sym.replace("USDT","")}</span>
<span className="text-slate-400 ml-1">{v.total} {v.win_rate}%</span> <span className="text-slate-400 ml-1">{v.count}</span>
<span className={`ml-1 font-mono font-bold ${v.total_pnl >= 0 ? "text-emerald-600" : "text-red-500"}`}>{v.total_pnl >= 0 ? "+" : ""}{v.total_pnl}R</span> <div className="font-mono mt-0.5">
<span>P95: {v.slippage_bps?.p95}bps</span>
<span className="ml-2">P95: {fmtMs(v.protection_gap_ms?.p95||0)}</span>
</div>
</div> </div>
))} ))}
</div> </div>
@ -315,7 +298,269 @@ function StatsPanel() {
); );
} }
// ─── 主页面 ────────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════
// L5: 对账面板
// ═══════════════════════════════════════════════════════════════
function L5_Reconciliation() {
const [data, setData] = useState<any>(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 (
<div className={`rounded-xl border ${ok ? "border-slate-200" : "border-red-300"} bg-white overflow-hidden`}>
<div className={`px-3 py-2 border-b ${ok ? "border-slate-100" : "border-red-200 bg-red-50"}`}>
<h3 className="font-semibold text-slate-800 text-xs">L5 {ok ? <span className="text-emerald-500"> </span> : <span className="text-red-500"> </span>}</h3>
</div>
<div className="p-3 grid grid-cols-2 gap-4 text-[11px]">
<div>
<p className="text-slate-400 mb-1"> ({data.local_positions?.length || 0})</p>
{(data.local_positions || []).map((p: any) => (
<div key={p.id} className="font-mono">{p.symbol?.replace("USDT","")} {p.direction} @ {fmtPrice(p.entry_price)}</div>
))}
{!data.local_positions?.length && <div className="text-slate-300"></div>}
</div>
<div>
<p className="text-slate-400 mb-1"> ({data.exchange_positions?.length || 0})</p>
{(data.exchange_positions || []).map((p: any, i: number) => (
<div key={i} className="font-mono">{p.symbol?.replace("USDT","")} {p.direction} qty={p.amount} liq={fmtPrice(p.liquidation_price)}</div>
))}
{!data.exchange_positions?.length && <div className="text-slate-300"></div>}
</div>
</div>
<div className="px-3 pb-2 text-[10px] text-slate-400">
挂单: 本地预期 {data.local_orders||0} / {data.exchange_orders||0}
</div>
{data.diffs?.length > 0 && (
<div className="px-3 pb-3 space-y-1">
{data.diffs.map((d: any, i: number) => (
<div key={i} className={`text-[10px] px-2 py-1 rounded ${d.severity==="critical"?"bg-red-50 text-red-700":"bg-amber-50 text-amber-700"}`}>
[{d.symbol}] {d.detail}
</div>
))}
</div>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L6: 风控状态
// ═══════════════════════════════════════════════════════════════
function L6_RiskStatus() {
const [risk, setRisk] = useState<any>(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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">L6 </h3></div>
<div className="p-3">
<div className="grid grid-cols-3 gap-2 text-[11px] mb-2">
{thresholds.map((t, i) => (
<div key={i} className="flex items-center gap-1"><span>{t.status}</span><span className="text-slate-600">{t.rule}</span></div>
))}
</div>
{risk.circuit_break_reason && (
<div className="text-[10px] bg-red-50 text-red-700 px-2 py-1.5 rounded">
<span className="font-bold"></span>{risk.circuit_break_reason}
{risk.auto_resume_time && <span className="ml-2 text-slate-500">: {new Date(risk.auto_resume_time * 1000).toLocaleTimeString("zh-CN")}</span>}
</div>
)}
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L8: 实盘 vs 模拟盘对照
// ═══════════════════════════════════════════════════════════════
function L8_PaperComparison() {
const [data, setData] = useState<any>(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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100">
<h3 className="font-semibold text-slate-800 text-xs">L8 vs <span className="text-[10px] text-slate-400">R差: {data.avg_pnl_diff_r}R</span></h3>
</div>
<div className="max-h-48 overflow-y-auto">
<table className="w-full text-[10px]">
<thead className="bg-slate-50 sticky top-0"><tr className="text-slate-500">
<th className="px-2 py-1 text-left"></th><th className="px-2 py-1"></th>
<th className="px-2 py-1 text-right"></th><th className="px-2 py-1 text-right"></th><th className="px-2 py-1 text-right">bps</th>
<th className="px-2 py-1 text-right">PnL</th><th className="px-2 py-1 text-right">PnL</th><th className="px-2 py-1 text-right">R差</th>
</tr></thead>
<tbody className="divide-y divide-slate-50">
{data.data.map((r: any, i: number) => (
<tr key={i} className="hover:bg-slate-50">
<td className="px-2 py-1 font-mono">{r.symbol?.replace("USDT","")}</td>
<td className={`px-2 py-1 text-center font-bold ${r.direction==="LONG"?"text-emerald-600":"text-red-500"}`}>{r.direction}</td>
<td className="px-2 py-1 text-right font-mono">{r.live_entry ? fmtPrice(r.live_entry) : "-"}</td>
<td className="px-2 py-1 text-right font-mono">{r.paper_entry ? fmtPrice(r.paper_entry) : "-"}</td>
<td className="px-2 py-1 text-right font-mono">{r.entry_diff_bps || "-"}</td>
<td className={`px-2 py-1 text-right font-mono ${(r.live_pnl||0)>=0?"text-emerald-600":"text-red-500"}`}>{r.live_pnl?.toFixed(2) || "-"}</td>
<td className={`px-2 py-1 text-right font-mono ${(r.paper_pnl||0)>=0?"text-emerald-600":"text-red-500"}`}>{r.paper_pnl?.toFixed(2) || "-"}</td>
<td className={`px-2 py-1 text-right font-mono font-bold ${(r.pnl_diff_r||0)>=0?"text-emerald-600":"text-red-500"}`}>{r.pnl_diff_r?.toFixed(2) || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L9: 权益曲线+回撤
// ═══════════════════════════════════════════════════════════════
function L9_EquityCurve() {
const [data, setData] = useState<any[]>([]);
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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">L9 线 + </h3></div>
<div className="p-2" style={{ height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={withDD}>
<XAxis dataKey="ts" tickFormatter={v => bjt(v)} tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} tickFormatter={v => `${v}R`} />
<Tooltip labelFormatter={v => bjt(Number(v))} />
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="3 3" />
<Area type="monotone" dataKey="pnl" name="累计PnL" stroke="#10b981" fill="#d1fae5" strokeWidth={2} />
<Area type="monotone" dataKey="dd" name="回撤" stroke="#ef4444" fill="#fee2e2" strokeWidth={1} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L10: 历史交易表
// ═══════════════════════════════════════════════════════════════
type FS = "all"|"BTC"|"ETH"|"XRP"|"SOL";
type FR = "all"|"win"|"loss";
function L10_TradeHistory() {
const [trades, setTrades] = useState<any[]>([]);
const [symbol, setSymbol] = useState<FS>("all");
const [result, setResult] = useState<FR>("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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between flex-wrap gap-1">
<h3 className="font-semibold text-slate-800 text-xs">L10 </h3>
<div className="flex gap-1">
{(["all","BTC","ETH","XRP","SOL"] as FS[]).map(s => (<button key={s} onClick={()=>setSymbol(s)} className={`px-2 py-0.5 rounded text-[10px] ${symbol===s?"bg-slate-800 text-white":"text-slate-500 hover:bg-slate-100"}`}>{s==="all"?"全部":s}</button>))}
<span className="text-slate-300">|</span>
{(["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 className="max-h-72 overflow-y-auto">
{trades.length === 0 ? <div className="text-center text-slate-400 text-sm py-6"></div> : (
<table className="w-full text-[10px]">
<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-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"></th><th className="px-1.5 py-1 text-right"></th><th className="px-1.5 py-1 text-right"></th>
</tr></thead>
<tbody className="divide-y divide-slate-50">
{trades.map((t: any) => {
const hm = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts-t.entry_ts)/60000) : 0;
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 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">{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 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-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>
</tr>);
})}
</tbody>
</table>
)}
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L11: 系统健康
// ═══════════════════════════════════════════════════════════════
function L11_SystemHealth() {
const [data, setData] = useState<any>(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 (
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div className="px-3 py-2 border-b border-slate-100"><h3 className="font-semibold text-slate-800 text-xs">L11 </h3></div>
<div className="p-3">
{Object.keys(procs).length > 0 && (
<div className="mb-2">
<p className="text-[10px] text-slate-400 mb-1"></p>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-1.5">
{Object.entries(procs).map(([name, p]: [string, any]) => (
<div key={name} className={`rounded-lg px-2 py-1 text-[10px] ${p.status==="online"?"bg-emerald-50":"bg-red-50"}`}>
<span className="font-medium">{name}</span>
<span className={`ml-1 ${p.status==="online"?"text-emerald-600":"text-red-500"}`}>{p.status}</span>
<span className="text-slate-400 ml-1">{p.memory_mb}MB {p.restarts}</span>
</div>
))}
</div>
</div>
)}
{fresh.market_data && (
<div className="text-[10px]">
<span className="text-slate-400">: </span>
<span className={`font-mono ${fresh.market_data.status==="green"?"text-emerald-600":fresh.market_data.status==="yellow"?"text-amber-500":"text-red-500"}`}>
{fresh.market_data.age_sec} {fresh.market_data.status==="green"?"✓":"⚠"}
</span>
</div>
)}
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// 主页面
// ═══════════════════════════════════════════════════════════════
export default function LiveTradingPage() { export default function LiveTradingPage() {
const { isLoggedIn, loading } = useAuth(); const { isLoggedIn, loading } = useAuth();
if (loading) return <div className="text-center text-slate-400 py-8">...</div>; if (loading) return <div className="text-center text-slate-400 py-8">...</div>;
@ -326,20 +571,25 @@ export default function LiveTradingPage() {
<Link href="/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm"></Link> <Link href="/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm"></Link>
</div> </div>
); );
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<L0_RiskBar />
<div className="flex items-center justify-between">
<div> <div>
<h1 className="text-lg font-bold text-slate-900"> </h1> <h1 className="text-lg font-bold text-slate-900"> </h1>
<p className="text-[10px] text-slate-500">V5.2 · USDT永续合约 · </p> <p className="text-[10px] text-slate-500">V5.2 · USDT永续合约 · </p>
</div> </div>
<RiskStatusPanel /> </div>
<EmergencyPanel /> <L1_EmergencyPanel />
<SummaryCards /> <L2_AccountOverview />
<ActivePositions /> <L3_Positions />
<EquityCurve /> <L4_ExecutionQuality />
<TradeHistory /> <L5_Reconciliation />
<StatsPanel /> <L6_RiskStatus />
<L8_PaperComparison />
<L9_EquityCurve />
<L10_TradeHistory />
<L11_SystemHealth />
</div> </div>
); );
} }