arbitrage-engine/frontend/app/live/page.tsx
root cb869926e2 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模拟盘
2026-03-02 09:38:14 +00:00

595 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { authFetch, useAuth } from "@/lib/auth";
import { XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart } from "recharts";
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 }); }
function fmtMs(ms: number) { return ms > 999 ? `${(ms/1000).toFixed(1)}s` : `${ms}ms`; }
const LIVE_STRATEGY = "v52_8signals";
// ═══════════════════════════════════════════════════════════════
// L0: 顶部固定风险条sticky永远可见
// ═══════════════════════════════════════════════════════════════
function L0_RiskBar() {
const [risk, setRisk] = useState<any>(null);
const [recon, setRecon] = useState<any>(null);
const [account, setAccount] = useState<any>(null);
useEffect(() => {
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);
}, []);
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 (
<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 gap-1.5">
<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>
<span className="text-slate-400"></span>
<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 className="flex items-center gap-3">
<div className="flex items-center gap-1">
<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 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>
);
}
// ═══════════════════════════════════════════════════════════════
// L1: 一键止血区
// ═══════════════════════════════════════════════════════════════
function L1_EmergencyPanel() {
const [confirming, setConfirming] = useState<string|null>(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 ? (
<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 (
<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">
<h3 className="font-semibold text-slate-800 text-xs"> </h3>
<div className="flex gap-2 items-center">
<ConfirmBtn action="emergency-close" label="🔴 全平" color="bg-red-500 hover:bg-red-600" />
<ConfirmBtn action="block-new" label="🟡 禁新仓" color="bg-amber-500 hover:bg-amber-600" />
<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>
{msg && <p className="text-[10px] text-blue-600 mt-1">{msg}</p>}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L2: 账户概览8卡片
// ═══════════════════════════════════════════════════════════════
function L2_AccountOverview() {
const [data, setData] = useState<any>(null);
const [summary, setSummary] = useState<any>(null);
useEffect(() => {
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);
}, []);
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 (
<div className="grid grid-cols-4 lg:grid-cols-8 gap-1.5">
{cards.map((c, i) => (
<div key={i} className="bg-white rounded-lg border border-slate-200 px-2 py-1.5">
<p className="text-[9px] text-slate-400 truncate">{c.label}</p>
<p className={`font-mono font-bold text-sm ${c.color || "text-slate-800"} truncate`}>{c.value}</p>
</div>
))}
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L3: 当前持仓WS实时
// ═══════════════════════════════════════════════════════════════
function L3_Positions() {
const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string,number>>({});
const [recon, setRecon] = useState<any>(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 {}
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 ws = new WebSocket(`wss://fstream.binance.com/stream?streams=${streams}`);
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<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>;
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">L3 <span className="text-[10px] text-emerald-500"> </span></h3>
</div>
<div className="divide-y divide-slate-100">
{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 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"?(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 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 (
<div key={p.id} className="px-3 py-2">
<div className="flex items-center justify-between">
<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-[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 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-[10px] ${unrealUsdt>=0?"text-emerald-500":"text-red-400"}`}>({unrealUsdt>=0?"+":""}${unrealUsdt.toFixed(2)})</span>
<span className={`text-[10px] ${holdColor}`}>{holdMin}m</span>
</div>
</div>
{/* 价格行 */}
<div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600 flex-wrap">
<span>入场: ${fmtPrice(entry)}</span>
<span>成交: ${fmtPrice(p.fill_price||entry)}</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">TP2: ${fmtPrice(p.tp2_price)}</span>
<span className="text-red-500">SL: ${fmtPrice(p.sl_price)}</span>
</div>
{/* 执行指标 */}
<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 ${(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">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">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>
</div>
</div>
);
})}
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════
// L4: 执行质量面板
// ═══════════════════════════════════════════════════════════════
function L4_ExecutionQuality() {
const [data, setData] = useState<any>(null);
useEffect(() => {
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 (
<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 (
<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">L4 <span className="text-[10px] text-slate-400">{data.total_trades}</span></h3></div>
<div className="p-3 space-y-2">
<MetricRow label="滑点" stat={o.slippage_bps} unit="bps" yellowThresh={2.5} redThresh={8} />
<MetricRow label="信号→下单" stat={o.signal_to_order_ms} unit="ms" yellowThresh={250} redThresh={1200} />
<MetricRow label="下单→成交" stat={o.order_to_fill_ms} unit="ms" yellowThresh={600} redThresh={3500} />
<MetricRow label="裸奔时间" stat={o.protection_gap_ms} unit="ms" yellowThresh={2000} redThresh={5000} />
</div>
{data.by_symbol && Object.keys(data.by_symbol).length > 0 && (
<div className="px-3 pb-3">
<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(data.by_symbol).map(([sym, v]: [string, any]) => (
<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="text-slate-400 ml-1">{v.count}</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>
);
}
// ═══════════════════════════════════════════════════════════════
// 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() {
const { isLoggedIn, loading } = useAuth();
if (loading) return <div className="text-center text-slate-400 py-8">加载中...</div>;
if (!isLoggedIn) return (
<div className="flex flex-col items-center justify-center h-64 gap-4">
<div className="text-5xl">🔒</div>
<p className="text-slate-600 font-medium">请先登录查看实盘</p>
<Link href="/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm">登录</Link>
</div>
);
return (
<div className="space-y-3">
<L0_RiskBar />
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-bold text-slate-900">⚡ 实盘交易</h1>
<p className="text-[10px] text-slate-500">V5.2策略 · 币安USDT永续合约 · 测试网</p>
</div>
</div>
<L1_EmergencyPanel />
<L2_AccountOverview />
<L3_Positions />
<L4_ExecutionQuality />
<L5_Reconciliation />
<L6_RiskStatus />
<L8_PaperComparison />
<L9_EquityCurve />
<L10_TradeHistory />
<L11_SystemHealth />
</div>
);
}