feat: /live 实盘交易前端页面

- 风控状态面板: 实时显示(正常/警告/熔断)、已实现R+未实现R+合计、连亏次数
- 紧急操作: 全平(双重确认)、禁止开仓、恢复交易
- 总览卡片: 盈亏R+USDT、胜率、持仓数、PF、手续费、资金费
- 当前持仓: WebSocket实时价格、滑点/裸奔/延迟指标、OrderID
- 权益曲线: Recharts AreaChart
- 历史交易: 含成交价/滑点/费用列、币种/盈亏筛选
- 详细统计: 滑点P50/P95/均值、按币种分组
- 导航栏: 新增实盘入口(Bolt图标)

风格与模拟盘一致: 白底+slate+emerald/red配色
This commit is contained in:
root 2026-03-02 09:28:07 +00:00
parent 832f78a1d7
commit 1ef1f97b5d
2 changed files with 332 additions and 116 deletions

View File

@ -1,130 +1,345 @@
"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";
import { useEffect, useState, useCallback } from "react";
import { api } from "@/lib/api";
import {
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
ResponsiveContainer, ReferenceLine, CartesianGrid
} from "recharts";
interface ChartPoint {
time: string;
btcRate: number;
ethRate: number;
btcPrice: number;
ethPrice: number;
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 });
}
export default function LivePage() {
const [data, setData] = useState<ChartPoint[]>([]);
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(true);
const [hours, setHours] = useState(2);
const fetchSnapshots = useCallback(async () => {
try {
const json = await api.snapshots(hours, 3600);
const rows = json.data || [];
setCount(json.count || 0);
// 降采样每30条取1条避免图表过密
const step = Math.max(1, Math.floor(rows.length / 300));
const sampled = rows.filter((_, i) => i % step === 0);
setData(sampled.map(row => ({
time: new Date(row.ts * 1000).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
btcRate: parseFloat((row.btc_rate * 100).toFixed(5)),
ethRate: parseFloat((row.eth_rate * 100).toFixed(5)),
btcPrice: row.btc_price,
ethPrice: row.eth_price,
})));
} catch { /* ignore */ } finally {
setLoading(false);
}
}, [hours]);
const LIVE_STRATEGY = "v52_8signals";
// ─── 风控状态 ────────────────────────────────────────────────────
function RiskStatusPanel() {
const [risk, setRisk] = useState<any>(null);
useEffect(() => {
fetchSnapshots();
const iv = setInterval(fetchSnapshots, 10_000);
return () => clearInterval(iv);
}, [fetchSnapshots]);
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 statusColor = risk.status === "normal" ? "border-emerald-400 bg-emerald-50" : risk.status === "warning" ? "border-amber-400 bg-amber-50" : risk.status === "circuit_break" ? "border-red-400 bg-red-50" : "border-slate-200 bg-slate-50";
const statusIcon = risk.status === "normal" ? "🟢" : risk.status === "warning" ? "🟡" : risk.status === "circuit_break" ? "🔴" : "⚪";
return (
<div className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-3">
<div className={`rounded-xl border-2 ${statusColor} px-4 py-3`}>
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-3">
<span className="text-lg">{statusIcon}</span>
<div>
<h1 className="text-2xl font-bold text-slate-900"></h1>
<p className="text-slate-500 text-sm mt-1">
8线 · <span className="text-blue-600 font-medium">{count.toLocaleString()}</span>
</p>
</div>
<div className="flex gap-2 text-sm">
{[1, 2, 6, 12, 24].map(h => (
<button
key={h}
onClick={() => setHours(h)}
className={`px-3 py-1.5 rounded-lg border transition-colors ${hours === h ? "bg-blue-600 text-white border-blue-600" : "border-slate-200 text-slate-600 hover:border-blue-400"}`}
>
{h}h
</button>
))}
<span className="font-bold text-sm text-slate-800">: {risk.status === "normal" ? "正常" : risk.status === "warning" ? "警告" : risk.status === "circuit_break" ? "熔断中" : "未知"}</span>
{risk.circuit_break_reason && <p className="text-[10px] text-red-600 mt-0.5">{risk.circuit_break_reason}</p>}
</div>
</div>
{loading ? (
<div className="text-slate-400 py-12 text-center">...</div>
) : data.length === 0 ? (
<div className="rounded-xl border border-slate-200 bg-slate-50 p-12 text-center text-slate-400">
2
<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><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><span className="text-slate-400"></span><p className="font-bold text-slate-800">{risk.consecutive_losses||0}</p></div>
</div>
) : (
<>
{/* 费率图 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
<h2 className="text-slate-700 font-semibold mb-1"></h2>
<p className="text-slate-400 text-xs mb-4">===</p>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
<YAxis tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} axisLine={false}
tickFormatter={v => `${v.toFixed(3)}%`} width={60} />
<Tooltip formatter={(v) => [`${Number(v).toFixed(5)}%`]}
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="4 2" />
<Line type="monotone" dataKey="btcRate" name="BTC费率" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
<Line type="monotone" dataKey="ethRate" name="ETH费率" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
</LineChart>
</ResponsiveContainer>
</div>
{/* 价格图 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
<h2 className="text-slate-700 font-semibold mb-1"></h2>
<p className="text-slate-400 text-xs mb-4"></p>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
<YAxis yAxisId="btc" orientation="left" tick={{ fill: "#2563eb", fontSize: 10 }} tickLine={false} axisLine={false}
tickFormatter={v => `$${(v/1000).toFixed(0)}k`} width={55} />
<YAxis yAxisId="eth" orientation="right" tick={{ fill: "#7c3aed", fontSize: 10 }} tickLine={false} axisLine={false}
tickFormatter={v => `$${v.toFixed(0)}`} width={55} />
<Tooltip formatter={(v, name) => [name?.toString().includes("BTC") ? `$${Number(v).toLocaleString()}` : `$${Number(v).toFixed(2)}`, name]}
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Line yAxisId="btc" type="monotone" dataKey="btcPrice" name="BTC价格" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
<Line yAxisId="eth" type="monotone" dataKey="ethPrice" name="ETH价格" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
</LineChart>
</ResponsiveContainer>
{(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 className="rounded-lg border border-blue-100 bg-blue-50 px-5 py-3 text-sm text-slate-600">
<span className="text-blue-600 font-medium"></span>
2Binance拉取实时溢价指数8
</div>
</>
)}
</div>
);
}
// ─── 紧急操作 ────────────────────────────────────────────────────
function 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("操作失败"); }
};
return (
<div className="rounded-xl border border-slate-200 bg-white px-4 py-3">
<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">
{confirming === "emergency-close" ? (
<div className="flex items-center gap-1">
<span className="text-[10px] text-red-600 font-medium"></span>
<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>
{msg && <p className="text-[10px] text-blue-600 mt-1">{msg}</p>}
</div>
);
}
// ─── 总览 ────────────────────────────────────────────────────────
function SummaryCards() {
const [data, setData] = useState<any>(null);
useEffect(() => {
const f = async () => { try { const r = await authFetch(`/api/live/summary?strategy=${LIVE_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} };
f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
}, []);
if (!data) return <div className="text-center text-slate-400 text-sm py-4">...</div>;
return (
<div className="grid grid-cols-3 lg:grid-cols-7 gap-1.5">
<div className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
<p className="text-[10px] text-slate-400">(R)</p>
<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={`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>
</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>
);
}
// ─── 当前持仓 ────────────────────────────────────────────────────
function ActivePositions() {
const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string, number>>({});
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 {} };
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 msg = JSON.parse(e.data); if (msg.data) { setWsPrices(prev => ({ ...prev, [msg.data.s]: parseFloat(msg.data.p) })); } } catch {} };
return () => ws.close();
}, []);
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"> <span className="text-[10px] text-emerald-500 font-normal"> </span> <span className="text-[10px] text-slate-400 font-normal ml-2"></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 currentPrice = wsPrices[p.symbol] || p.current_price || 0;
const entry = p.entry_price || 0;
const rd = p.risk_distance || 1;
const fullR = rd > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / rd : (entry - currentPrice) / rd) : 0;
const tp1R = rd > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / rd : (entry - (p.tp1_price || 0)) / rd) : 0;
const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR;
const unrealUsdt = unrealR * 2;
const holdColor = holdMin >= 60 ? "text-red-500 font-bold" : holdMin >= 45 ? "text-amber-500" : "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>
</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">现价: ${currentPrice ? fmtPrice(currentPrice) : "-"}</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"}`}> {p.protection_gap_ms||0}ms</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"> {p.order_to_fill_ms||0}ms</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>
);
}
// ─── 权益曲线 ────────────────────────────────────────────────────
function 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;
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);
useEffect(() => {
const f = async () => { try { const r = await authFetch(`/api/live/stats?strategy=${LIVE_STRATEGY}`); if (r.ok) setData(await r.json()); } catch {} };
f(); const iv = setInterval(f, 30000); return () => clearInterval(iv);
}, []);
if (!data || data.error) 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"></h3></div>
<div className="p-3 grid grid-cols-2 lg:grid-cols-4 gap-2 text-xs">
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_rate}%</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold">{data.win_loss_ratio}</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-emerald-600">+{data.avg_win}R</p></div>
<div><span className="text-slate-400"></span><p className="font-mono font-bold text-red-500">-{data.avg_loss}R</p></div>
<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>
{data.by_symbol && (
<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-2">
{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]">
<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={`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>
))}
</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">
<div>
<h1 className="text-lg font-bold text-slate-900"> </h1>
<p className="text-[10px] text-slate-500">V5.2 · USDT永续合约 · </p>
</div>
<RiskStatusPanel />
<EmergencyPanel />
<SummaryCards />
<ActivePositions />
<EquityCurve />
<TradeHistory />
<StatsPanel />
</div>
);
}

View File

@ -7,12 +7,13 @@ import { useAuth } from "@/lib/auth";
import {
LayoutDashboard, Info,
Menu, X, Zap, LogIn, UserPlus,
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles
ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor, LineChart, Sparkles, Bolt
} from "lucide-react";
const navItems = [
{ href: "/", label: "仪表盘", icon: LayoutDashboard },
{ href: "/trades", label: "成交流", icon: Activity },
{ href: "/live", label: "⚡ 实盘交易", icon: Bolt, section: "── 实盘 ──" },
{ href: "/signals", label: "V5.1 信号引擎", icon: Crosshair, section: "── V5.1 ──" },
{ href: "/paper", label: "V5.1 模拟盘", icon: LineChart },
{ href: "/signals-v52", label: "V5.2 信号引擎", icon: Sparkles, section: "── V5.2 ──" },