arbitrage-engine/frontend/app/paper-v53/page.tsx

442 lines
23 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 parseFactors(raw: any) {
if (!raw) return null;
if (typeof raw === "string") { try { return JSON.parse(raw); } catch { return null; } }
return raw;
}
type StrategyTab = "v53_alt" | "v53_btc";
const STRATEGY_LABELS: Record<StrategyTab, { label: string; desc: string; coins: string[]; badgeClass: string }> = {
v53_alt: {
label: "🟣 ALT轨 (v53_alt)",
desc: "ETH / XRP / SOL · 四层评分 55/25/15/5",
coins: ["ETHUSDT", "XRPUSDT", "SOLUSDT"],
badgeClass: "bg-purple-100 text-purple-700 border border-purple-200",
},
v53_btc: {
label: "🔵 BTC轨 (v53_btc)",
desc: "BTCUSDT · Gate-Control逻辑",
coins: ["BTCUSDT"],
badgeClass: "bg-amber-100 text-amber-700 border border-amber-200",
},
};
// ─── 控制面板 ────────────────────────────────────────────────────
function ControlPanel() {
const [config, setConfig] = useState<any>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
(async () => {
try { const r = await authFetch("/api/paper/config"); if (r.ok) setConfig(await r.json()); } catch {}
})();
}, []);
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: any) => 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: StrategyTab }) {
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-6 gap-1.5">
{[
{ label: "总盈亏(R)", value: `${data.total_pnl >= 0 ? "+" : ""}${data.total_pnl}R`, sub: `${data.total_pnl_usdt >= 0 ? "+" : ""}$${data.total_pnl_usdt}`, color: data.total_pnl >= 0 ? "text-emerald-600" : "text-red-500" },
{ label: "胜率", value: `${data.win_rate}%`, sub: `${data.total_trades}`, color: "text-slate-800" },
{ label: "持仓中", value: data.active_positions, sub: "活跃仓位", color: "text-blue-600" },
{ label: "盈亏比", value: data.profit_factor, sub: "Profit Factor", color: "text-slate-800" },
{ label: "当前资金", value: `$${data.balance?.toLocaleString()}`, sub: "虚拟余额", color: data.balance >= 10000 ? "text-emerald-600" : "text-red-500" },
{ label: "状态", value: data.start_time ? "运行中 ✅" : "等待首笔", sub: "signal accumulating", color: "text-slate-600" },
].map(({ label, value, sub, color }) => (
<div key={label} className="bg-white rounded-lg border border-slate-200 px-2.5 py-2">
<p className="text-[10px] text-slate-400">{label}</p>
<p className={`font-mono font-bold text-base ${color}`}>{value}</p>
<p className="text-[10px] text-slate-400">{sub}</p>
</div>
))}
</div>
);
}
// ─── 当前持仓 ────────────────────────────────────────────────────
function ActivePositions({ strategy }: { strategy: StrategyTab }) {
const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string, number>>({});
const [paperRiskUsd, setPaperRiskUsd] = useState(200);
const meta = STRATEGY_LABELS[strategy];
useEffect(() => {
(async () => {
try { const r = await authFetch("/api/paper/config"); if (r.ok) { const cfg = await r.json(); setPaperRiskUsd((cfg.initial_balance || 10000) * (cfg.risk_per_trade || 0.02)); } } catch {}
})();
}, []);
useEffect(() => {
const f = async () => {
try { const r = await authFetch(`/api/paper/positions?strategy=${strategy}`); if (r.ok) setPositions((await r.json()).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">
{meta.label}
</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 entry = p.entry_price || 0;
const riskDist = p.risk_distance || Math.abs(entry - (p.sl_price || entry)) || 1;
const tp1R = riskDist > 0 ? (p.direction === "LONG" ? ((p.tp1_price || 0) - entry) / riskDist : (entry - (p.tp1_price || 0)) / riskDist) : 0;
const fullR = riskDist > 0 ? (p.direction === "LONG" ? (currentPrice - entry) / riskDist : (entry - currentPrice) / riskDist) : 0;
const unrealR = p.tp1_hit ? 0.5 * tp1R + 0.5 * fullR : fullR;
const unrealUsdt = unrealR * paperRiskUsd;
return (
<div key={p.id} className="px-3 py-2 bg-emerald-50/60">
<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 ${meta.badgeClass}`}>{strategy}</span>
<span className="text-[10px] text-slate-500">{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(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>
</div>
</div>
);
})}
</div>
</div>
);
}
// ─── 权益曲线 ────────────────────────────────────────────────────
function EquityCurve({ strategy }: { strategy: StrategyTab }) {
const [data, setData] = useState<any[]>([]);
useEffect(() => {
const f = async () => {
try { const r = await authFetch(`/api/paper/equity-curve?strategy=${strategy}`); if (r.ok) setData((await r.json()).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">V5.3 ...</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 FilterResult = "all" | "win" | "loss";
function TradeHistory({ strategy }: { strategy: StrategyTab }) {
const [trades, setTrades] = useState<any[]>([]);
const [result, setResult] = useState<FilterResult>("all");
const meta = STRATEGY_LABELS[strategy];
useEffect(() => {
const f = async () => {
try {
const r = await authFetch(`/api/paper/trades?result=${result}&strategy=${strategy}&limit=50`);
if (r.ok) setTrades((await r.json()).data || []);
} catch {}
};
f(); const iv = setInterval(f, 10000); return () => clearInterval(iv);
}, [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 ${meta.badgeClass}`}>{strategy}</span>
<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-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 track = factors?.track || (t.symbol === "BTCUSDT" ? "BTC" : "ALT");
return (
<tr key={t.id} className="hover:bg-slate-50">
<td className="px-2 py-1.5 font-mono">
{t.symbol?.replace("USDT", "")}
<span className={`ml-1 text-[9px] px-1 rounded ${track === "BTC" ? "bg-amber-100 text-amber-700" : "bg-purple-100 text-purple-700"}`}>{track}</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">{t.score}</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: StrategyTab }) {
const [data, setData] = useState<any>(null);
const [tab, setTab] = useState("ALL");
useEffect(() => { setTab("ALL"); }, [strategy]);
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]);
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 coinTabs = strategy === "v53_btc" ? ["ALL", "BTC"] : ["ALL", "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 flex-wrap gap-1">
<h3 className="font-semibold text-slate-800 text-xs"></h3>
<div className="flex items-center gap-1">
{coinTabs.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">
<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>
</div>
</div>
) : (
<div className="p-3 text-xs text-slate-400"></div>
)}
</div>
);
}
// ─── 主页面 ──────────────────────────────────────────────────────
export default function PaperTradingV53Page() {
const { isLoggedIn, loading } = useAuth();
const [strategyTab, setStrategyTab] = useState<StrategyTab>("v53_alt");
const meta = STRATEGY_LABELS[strategyTab];
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="flex items-center justify-between flex-wrap gap-2">
<div>
<h1 className="text-lg font-bold text-slate-900">📈 V5.3</h1>
<p className="text-[10px] text-slate-500">ALT轨(ETH/XRP/SOL) + BTC独立Gate-Control</p>
</div>
{/* 策略Tab切换 */}
<div className="flex gap-1.5">
{(Object.entries(STRATEGY_LABELS) as [StrategyTab, typeof STRATEGY_LABELS[StrategyTab]][]).map(([key, val]) => (
<button key={key} onClick={() => setStrategyTab(key)}
className={`px-3 py-1.5 rounded-lg border text-xs font-medium transition-all ${strategyTab === key ? val.badgeClass + " border-current" : "border-slate-200 text-slate-600 hover:border-slate-400"}`}>
{val.label}
</button>
))}
</div>
</div>
<div className={`rounded-lg border px-3 py-1.5 text-[11px] ${meta.badgeClass}`}>
<span className="font-semibold">{meta.label}</span> {meta.desc}
</div>
<ControlPanel />
<SummaryCards strategy={strategyTab} />
<ActivePositions strategy={strategyTab} />
<EquityCurve strategy={strategyTab} />
<TradeHistory strategy={strategyTab} />
<StatsPanel strategy={strategyTab} />
</div>
);
}