arbitrage-engine/frontend/app/paper/page.tsx
root ee90b8dcfa feat: sidebar navigation with V5.1/V5.2 separate entries
- Sidebar: 信号/模拟盘 section headers
- Three paper trade entries: 全部持仓, V5.1模拟盘, V5.2模拟盘 (NEW badge)
- Paper page reads strategy from URL query params
- Suspense boundary for useSearchParams
2026-03-01 12:25:40 +00:00

732 lines
32 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, Suspense } from "react";
import { useSearchParams } from "next/navigation";
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 parseFactors(raw: any) {
if (!raw) return null;
if (typeof raw === "string") {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
return raw;
}
type StrategyFilter = "all" | "v51_baseline" | "v52_8signals";
const STRATEGY_TABS: { value: StrategyFilter; label: string; hint: string }[] = [
{ value: "all", label: "全部", hint: "总览" },
{ value: "v51_baseline", label: "V5.1 模拟盘", hint: "经典五层" },
{ value: "v52_8signals", label: "V5.2 模拟盘", hint: "8信号 + FR/Liq" },
];
function normalizeStrategy(strategy: string | null | undefined): StrategyFilter {
if (strategy === "v52_8signals") return "v52_8signals";
if (strategy === "v51_baseline") return "v51_baseline";
return "v51_baseline";
}
function strategyName(strategy: string | null | undefined) {
const normalized = normalizeStrategy(strategy);
if (normalized === "v52_8signals") return "V5.2";
return "V5.1";
}
function strategyBadgeClass(strategy: string | null | undefined) {
return normalizeStrategy(strategy) === "v52_8signals"
? "bg-emerald-100 text-emerald-700 border border-emerald-200"
: "bg-slate-200 text-slate-700 border border-slate-300";
}
function strategyBadgeText(strategy: string | null | undefined) {
return normalizeStrategy(strategy) === "v52_8signals" ? "✨ V5.2" : "V5.1";
}
function strategyTabDescription(strategy: StrategyFilter) {
if (strategy === "all") return "全部策略合并视图";
if (strategy === "v52_8signals") return "仅展示 V5.2 数据(含 FR / Liq";
return "仅展示 V5.1 数据";
}
// ─── 控制面板(开关+配置)──────────────────────────────────────
function ControlPanel() {
const [config, setConfig] = useState<any>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
const f = async () => {
try {
const r = await authFetch("/api/paper/config");
if (r.ok) setConfig(await r.json());
} catch {}
};
f();
}, []);
const toggle = async () => {
setSaving(true);
try {
const r = await authFetch("/api/paper/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: !config.enabled }),
});
if (r.ok) setConfig(await r.json().then((j) => j.config));
} catch {
} finally {
setSaving(false);
}
};
if (!config) return null;
return (
<div className={`rounded-xl border-2 ${config.enabled ? "border-emerald-400 bg-emerald-50" : "border-slate-200 bg-white"} px-3 py-2 flex items-center justify-between`}>
<div className="flex items-center gap-3">
<button
onClick={toggle}
disabled={saving}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
config.enabled ? "bg-red-500 text-white hover:bg-red-600" : "bg-emerald-500 text-white hover:bg-emerald-600"
}`}
>
{saving ? "..." : config.enabled ? "⏹ 停止模拟盘" : "▶️ 启动模拟盘"}
</button>
<span className={`text-xs font-medium ${config.enabled ? "text-emerald-700" : "text-slate-500"}`}>
{config.enabled ? "🟢 运行中" : "⚪ 已停止"}
</span>
</div>
<div className="flex gap-4 text-[10px] text-slate-500">
<span>初始资金: ${config.initial_balance?.toLocaleString()}</span>
<span>: {(config.risk_per_trade * 100).toFixed(0)}%</span>
<span>: {config.max_positions}</span>
</div>
</div>
);
}
// ─── 总览面板 ────────────────────────────────────────────────────
function SummaryCards({ strategy }: { strategy: StrategyFilter }) {
const [data, setData] = useState<any>(null);
useEffect(() => {
const f = async () => {
try {
const r = await authFetch(`/api/paper/summary?strategy=${strategy}`);
if (r.ok) setData(await r.json());
} catch {}
};
f();
const iv = setInterval(f, 10000);
return () => clearInterval(iv);
}, [strategy]);
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"></p>
<p className={`font-mono font-bold text-base ${data.balance >= 10000 ? "text-emerald-600" : "text-red-500"}`}>
${data.balance?.toLocaleString()}
</p>
</div>
<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 >= 0 ? "text-emerald-600" : "text-red-500"}`}>
{data.total_pnl >= 0 ? "+" : ""}
{data.total_pnl}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-semibold text-sm text-slate-600">{data.start_time ? "运行中 ✅" : "等待首笔"}</p>
</div>
</div>
);
}
// ─── 最新信号状态 ────────────────────────────────────────────────
const COINS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"];
function LatestSignals() {
const [signals, setSignals] = useState<Record<string, any>>({});
useEffect(() => {
const f = async () => {
for (const sym of COINS) {
try {
const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT", "")}&limit=1`);
if (r.ok) {
const j = await r.json();
if (j.data && j.data.length > 0) {
setSignals((prev) => ({ ...prev, [sym]: j.data[0] }));
}
}
} catch {}
}
};
f();
const iv = setInterval(f, 15000);
return () => clearInterval(iv);
}, []);
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="divide-y divide-slate-50">
{COINS.map((sym) => {
const s = signals[sym];
const coin = sym.replace("USDT", "");
const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null;
return (
<div key={sym} className="px-3 py-1.5 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-bold text-slate-700 w-8">{coin}</span>
{s?.signal ? (
<>
<span className={`text-xs font-bold ${s.signal === "LONG" ? "text-emerald-600" : "text-red-500"}`}>
{s.signal === "LONG" ? "🟢" : "🔴"} {s.signal}
</span>
<span className="font-mono text-[10px] text-slate-500">{s.score}</span>
</>
) : (
<span className="text-[10px] text-slate-400"> </span>
)}
</div>
{ago !== null && <span className="text-[10px] text-slate-400">{ago < 60 ? `${ago}m前` : `${Math.round(ago / 60)}h前`}</span>}
</div>
);
})}
</div>
</div>
);
}
// ─── 当前持仓 ────────────────────────────────────────────────────
function ActivePositions({ strategy }: { strategy: StrategyFilter }) {
const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string, number>>({});
useEffect(() => {
const f = async () => {
try {
const r = await authFetch(`/api/paper/positions?strategy=${strategy}`);
if (r.ok) {
const j = await r.json();
setPositions(j.data || []);
}
} catch {}
};
f();
const iv = setInterval(f, 10000);
return () => clearInterval(iv);
}, [strategy]);
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) {
const sym = msg.data.s;
const price = parseFloat(msg.data.p);
if (sym && price > 0) setWsPrices((prev) => ({ ...prev, [sym]: price }));
}
} 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">
{strategy === "all" ? "暂无活跃持仓" : `${strategyName(strategy)} 暂无活跃持仓`}
</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>
</h3>
</div>
<div className="divide-y divide-slate-100">
{positions.map((p: any) => {
const sym = p.symbol?.replace("USDT", "") || "";
const holdMin = Math.round((Date.now() - p.entry_ts) / 60000);
const currentPrice = wsPrices[p.symbol] || p.current_price || 0;
const factors = parseFactors(p.score_factors);
const frScore = factors?.funding_rate?.score ?? 0;
const liqScore = factors?.liquidation?.score ?? 0;
const entry = p.entry_price || 0;
const atr = p.atr_at_entry || 1;
const riskDist = 2.0 * 0.7 * atr;
const fullR = riskDist > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / riskDist : (entry - currentPrice) / riskDist) : 0;
const tp1R = riskDist > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / riskDist : (entry - (p.tp1_price || 0)) / riskDist) : 0;
const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR;
const unrealUsdt = unrealR * 200;
const isV52 = normalizeStrategy(p.strategy) === "v52_8signals";
return (
<div key={p.id} className={`px-3 py-2 ${isV52 ? "bg-emerald-50/60" : "bg-slate-50/70"}`}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs font-bold ${p.direction === "LONG" ? "text-emerald-600" : "text-red-500"}`}>
{p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction}
</span>
<span className={`px-1.5 py-0.5 rounded text-[10px] font-semibold ${strategyBadgeClass(p.strategy)}`}>
{strategyBadgeText(p.strategy)}
</span>
<span className="text-[10px] text-slate-500">{p.score} · {p.tier === "heavy" ? "加仓" : p.tier === "standard" ? "标准" : "轻仓"}</span>
{isV52 && (
<span className="text-[10px] font-semibold text-emerald-700">FR {frScore >= 0 ? "+" : ""}{frScore} · Liq {liqScore >= 0 ? "+" : ""}{liqScore}</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(0)})
</span>
<span className="text-[10px] text-slate-400">{holdMin}m</span>
</div>
</div>
<div className="flex gap-3 mt-1 text-[10px] font-mono text-slate-600 flex-wrap">
<span>入场: ${fmtPrice(p.entry_price)}</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>
{!isV52 && <span className="text-slate-400">FR/Liq V5.2 </span>}
</div>
{isV52 && (
<div className="mt-1 grid grid-cols-2 gap-2 text-[10px] font-semibold">
<div className="rounded-md bg-emerald-100/70 text-emerald-800 px-2 py-1"> Funding Rate Score: {frScore >= 0 ? "+" : ""}{frScore}</div>
<div className="rounded-md bg-cyan-100/70 text-cyan-800 px-2 py-1"> Liquidation Score: {liqScore >= 0 ? "+" : ""}{liqScore}</div>
</div>
)}
</div>
);
})}
</div>
</div>
);
}
// ─── 权益曲线 ────────────────────────────────────────────────────
function EquityCurve({ strategy }: { strategy: StrategyFilter }) {
const [data, setData] = useState<any[]>([]);
useEffect(() => {
const f = async () => {
try {
const r = await authFetch(`/api/paper/equity-curve?strategy=${strategy}`);
if (r.ok) {
const j = await r.json();
setData(j.data || []);
}
} catch {}
};
f();
const iv = setInterval(f, 30000);
return () => clearInterval(iv);
}, [strategy]);
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>
{data.length < 2 ? (
<div className="px-3 py-6 text-center text-xs text-slate-400">{strategy === "all" ? "暂无足够历史数据" : `${strategyName(strategy)} 暂无足够历史数据`}</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({ strategy }: { strategy: StrategyFilter }) {
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/paper/trades?symbol=${symbol}&result=${result}&strategy=${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, strategy]);
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 items-center gap-1 flex-wrap">
<span className={`px-2 py-0.5 rounded text-[10px] font-semibold ${strategy === "all" ? "bg-slate-100 text-slate-600" : strategyBadgeClass(strategy)}`}>
{strategy === "all" ? "全部策略" : `${strategyBadgeText(strategy)} 视图`}
</span>
<span className="text-slate-300">|</span>
{(["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-64 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-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">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>
</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;
const factors = parseFactors(t.score_factors);
const frScore = factors?.funding_rate?.score ?? 0;
const liqScore = factors?.liquidation?.score ?? 0;
const isV52 = normalizeStrategy(t.strategy) === "v52_8signals";
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 text-[10px]">
<span className={`px-1.5 py-0.5 rounded font-semibold ${strategyBadgeClass(t.strategy)}`}>{strategyBadgeText(t.strategy)}</span>
</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.exit_price ? fmtPrice(t.exit_price) : "-"}</td>
<td className={`px-2 py-1.5 text-right font-mono font-bold ${t.pnl_r > 0 ? "text-emerald-600" : t.pnl_r < 0 ? "text-red-500" : "text-slate-500"}`}>
{t.pnl_r > 0 ? "+" : ""}
{t.pnl_r?.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"
: t.status === "signal_flip"
? "bg-purple-100 text-purple-700"
: "bg-slate-100 text-slate-600"
}`}
>
{t.status === "tp"
? "止盈"
: t.status === "sl"
? "止损"
: t.status === "sl_be"
? "保本"
: t.status === "timeout"
? "超时"
: t.status === "signal_flip"
? "翻转"
: t.status}
</span>
</td>
<td className="px-2 py-1.5 text-right font-mono">
<div>{t.score}</div>
<div className={`text-[9px] ${isV52 ? "text-emerald-600 font-semibold" : "text-slate-400"}`}>
{isV52 ? `✨ FR ${frScore >= 0 ? "+" : ""}${frScore} · Liq ${liqScore >= 0 ? "+" : ""}${liqScore}` : "FR/Liq 仅V5.2"}
</div>
</td>
<td className="px-2 py-1.5 text-right text-slate-400">{holdMin}m</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
);
}
// ─── 统计面板 ────────────────────────────────────────────────────
function StatsPanel({ strategy }: { strategy: StrategyFilter }) {
const [data, setData] = useState<any>(null);
const [tab, setTab] = useState("ALL");
useEffect(() => {
const f = async () => {
try {
const r = await authFetch(`/api/paper/stats?strategy=${strategy}`);
if (r.ok) setData(await r.json());
} catch {}
};
f();
const iv = setInterval(f, 30000);
return () => clearInterval(iv);
}, [strategy]);
useEffect(() => {
setTab("ALL");
}, [strategy]);
if (!data || data.error) {
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 text-xs text-slate-400"></div>
</div>
);
}
const tabs = ["ALL", "BTC", "ETH", "XRP", "SOL"];
const st = tab === "ALL" ? data : (data.by_symbol?.[tab] || 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 flex items-center justify-between">
<h3 className="font-semibold text-slate-800 text-xs"></h3>
<div className="flex items-center gap-1">
{strategy !== "all" && <span className={`px-2 py-0.5 rounded text-[10px] font-semibold ${strategyBadgeClass(strategy)}`}>{strategyBadgeText(strategy)}</span>}
{tabs.map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`px-2 py-0.5 rounded text-[10px] font-medium transition-colors ${tab === t ? "bg-slate-800 text-white" : "bg-slate-100 text-slate-500 hover:bg-slate-200"}`}
>
{t === "ALL" ? "总计" : t}
</button>
))}
</div>
</div>
{st ? (
<div className="p-3 space-y-3">
<div className="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">{st.win_rate}%</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold">{st.win_loss_ratio}</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold text-emerald-600">+{st.avg_win}R</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold text-red-500">-{st.avg_loss}R</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold">{st.mdd}R</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold">{st.sharpe}</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className={`font-mono font-bold ${(st.total_pnl ?? 0) >= 0 ? "text-emerald-600" : "text-red-500"}`}>
{(st.total_pnl ?? 0) >= 0 ? "+" : ""}
{st.total_pnl ?? "-"}R
</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono font-bold">{st.total ?? data.total}</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono">{st.long_win_rate}% ({st.long_count})</p>
</div>
<div>
<span className="text-slate-400"></span>
<p className="font-mono">{st.short_win_rate}% ({st.short_count})</p>
</div>
{tab === "ALL" && data.by_tier && Object.entries(data.by_tier).map(([t, v]: [string, any]) => (
<div key={t}>
<span className="text-slate-400">{t === "heavy" ? "加仓档" : t === "standard" ? "标准档" : "轻仓档"}</span>
<p className="font-mono">{v.win_rate}% ({v.total})</p>
</div>
))}
</div>
</div>
) : (
<div className="p-3 text-xs text-slate-400"></div>
)}
</div>
);
}
// ─── 主页面 ──────────────────────────────────────────────────────
function PaperTradingPageInner() {
const { isLoggedIn, loading } = useAuth();
const searchParams = useSearchParams();
const urlStrategy = searchParams.get("strategy");
const [strategyTab, setStrategyTab] = useState<StrategyFilter>(() => normalizeStrategy(urlStrategy));
// URL参数变化时同步
useEffect(() => {
if (urlStrategy) {
setStrategyTab(normalizeStrategy(urlStrategy));
}
}, [urlStrategy]);
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 className="rounded-2xl border-2 border-slate-200 bg-white p-2.5 shadow-sm">
<p className="text-[11px] font-semibold text-slate-500 mb-2"></p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{STRATEGY_TABS.map((tab) => (
<button
key={tab.value}
onClick={() => setStrategyTab(tab.value)}
className={`rounded-xl border-2 px-4 py-3 text-left transition-all ${
strategyTab === tab.value
? "border-slate-800 bg-slate-800 text-white shadow"
: "border-slate-100 bg-slate-100 text-slate-600 hover:bg-slate-200"
}`}
>
<p className="text-sm font-bold">{tab.label}</p>
<p className={`text-[10px] mt-0.5 ${strategyTab === tab.value ? "text-slate-200" : "text-slate-500"}`}>{tab.hint}</p>
</button>
))}
</div>
</div>
<div>
<h1 className="text-lg font-bold text-slate-900">📊 </h1>
<p className="text-[10px] text-slate-500">V5.2AB测试 · · · {strategyTabDescription(strategyTab)}</p>
</div>
<ControlPanel />
<SummaryCards strategy={strategyTab} />
<LatestSignals />
<ActivePositions strategy={strategyTab} />
<EquityCurve strategy={strategyTab} />
<TradeHistory strategy={strategyTab} />
<StatsPanel strategy={strategyTab} />
</div>
);
}
export default function PaperTradingPage() {
return (
<Suspense fallback={<div className="text-center text-slate-400 py-8">...</div>}>
<PaperTradingPageInner />
</Suspense>
);
}