- 风控状态面板: 实时显示(正常/警告/熔断)、已实现R+未实现R+合计、连亏次数 - 紧急操作: 全平(双重确认)、禁止开仓、恢复交易 - 总览卡片: 盈亏R+USDT、胜率、持仓数、PF、手续费、资金费 - 当前持仓: WebSocket实时价格、滑点/裸奔/延迟指标、OrderID - 权益曲线: Recharts AreaChart - 历史交易: 含成交价/滑点/费用列、币种/盈亏筛选 - 详细统计: 滑点P50/P95/均值、按币种分组 - 导航栏: 新增实盘入口(Bolt图标) 风格与模拟盘一致: 白底+slate+emerald/red配色
345 lines
24 KiB
TypeScript
345 lines
24 KiB
TypeScript
"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 });
|
||
}
|
||
|
||
const LIVE_STRATEGY = "v52_8signals";
|
||
|
||
// ─── 风控状态 ────────────────────────────────────────────────────
|
||
function RiskStatusPanel() {
|
||
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 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={`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>
|
||
<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>
|
||
<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>
|
||
{(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>
|
||
);
|
||
}
|
||
|
||
// ─── 紧急操作 ────────────────────────────────────────────────────
|
||
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>
|
||
);
|
||
} |