diff --git a/frontend/app/live/page.tsx b/frontend/app/live/page.tsx index 0e07515..e653372 100644 --- a/frontend/app/live/page.tsx +++ b/frontend/app/live/page.tsx @@ -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([]); - 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(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 ( -
-
-
-

实时费率变动

-

- 8小时结算周期内的实时费率曲线 · 已记录 {count.toLocaleString()} 条快照 -

+
+
+
+ {statusIcon} +
+ 风控: {risk.status === "normal" ? "正常" : risk.status === "warning" ? "警告" : risk.status === "circuit_break" ? "熔断中" : "未知"} + {risk.circuit_break_reason &&

{risk.circuit_break_reason}

} +
-
- {[1, 2, 6, 12, 24].map(h => ( - - ))} +
+
已实现

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_realized_r||0) >= 0 ? "+" : ""}{risk.today_realized_r||0}R

+
未实现

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_unrealized_r||0) >= 0 ? "+" : ""}{risk.today_unrealized_r||0}R

+
合计

= 0 ? "text-emerald-600" : "text-red-500"}`}>{(risk.today_total_r||0) >= 0 ? "+" : ""}{risk.today_total_r||0}R

+
连亏

{risk.consecutive_losses||0}次

- - {loading ? ( -
加载中...
- ) : data.length === 0 ? ( -
- 暂无数据(后端刚启动,2秒后开始积累) + {(risk.block_new_entries || risk.reduce_only) && ( +
+ {risk.block_new_entries && 🚫 禁止新开仓} + {risk.reduce_only && 🔒 只减仓}
- ) : ( - <> - {/* 费率图 */} -
-

资金费率实时变动

-

正值=多头付空头,负值=空头付多头。费率上升=多头情绪加热

- - - - - `${v.toFixed(3)}%`} width={60} /> - [`${Number(v).toFixed(5)}%`]} - contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} /> - - - - - - -
- - {/* 价格图 */} -
-

标记价格走势

-

与费率对比观察:价格快速拉升时,资金费率通常同步上涨(多头加杠杆)

- - - - - `$${(v/1000).toFixed(0)}k`} width={55} /> - `$${v.toFixed(0)}`} width={55} /> - [name?.toString().includes("BTC") ? `$${Number(v).toLocaleString()}` : `$${Number(v).toFixed(2)}`, name]} - contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} /> - - - - - -
- - {/* 说明 */} -
- 数据说明: - 每2秒从Binance拉取实时溢价指数,本地永久存储。这是8小时结算周期内费率变动的原始数据,不在任何公开数据源中提供。 -
- )}
); } + +// ─── 紧急操作 ──────────────────────────────────────────────────── +function EmergencyPanel() { + const [confirming, setConfirming] = useState(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 ( +
+
+

⚡ 紧急操作

+
+ {confirming === "emergency-close" ? ( +
+ 确认全平? + + +
+ ) : ( + + )} + {confirming === "block-new" ? ( +
+ 确认? + + +
+ ) : ( + + )} + +
+
+ {msg &&

{msg}

} +
+ ); +} + +// ─── 总览 ──────────────────────────────────────────────────────── +function SummaryCards() { + const [data, setData] = useState(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
加载中...
; + return ( +
+
+

总盈亏(R)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl_r >= 0 ? "+" : ""}{data.total_pnl_r}R

+

= 0 ? "text-emerald-500" : "text-red-400"}`}>{data.total_pnl_usdt >= 0 ? "+" : ""}${data.total_pnl_usdt}

+
+

胜率

{data.win_rate}%

+

总交易

{data.total_trades}

+

持仓中

{data.active_positions}

+

盈亏比(PF)

{data.profit_factor}

+

手续费

${data.total_fee_usdt}

+

资金费

${data.total_funding_usdt}

+
+ ); +} + +// ─── 当前持仓 ──────────────────────────────────────────────────── +function ActivePositions() { + const [positions, setPositions] = useState([]); + const [wsPrices, setWsPrices] = useState>({}); + 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
暂无活跃持仓
; + + return ( +
+
+

当前持仓 ● 实时 币安合约

+
+
+ {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 ( +
+
+
+ {p.direction === "LONG" ? "🟢" : "🔴"} {sym} {p.direction} + 评分{p.score} · {p.tier === "heavy" ? "加仓" : "标准"} +
+
+ = 0 ? "text-emerald-600" : "text-red-500"}`}>{unrealR >= 0 ? "+" : ""}{unrealR.toFixed(2)}R + = 0 ? "text-emerald-500" : "text-red-400"}`}>({unrealUsdt >= 0 ? "+" : ""}${unrealUsdt.toFixed(2)}) + {holdMin}m +
+
+
+ 入场: ${fmtPrice(entry)} + 成交: ${fmtPrice(p.fill_price || entry)} + 现价: ${currentPrice ? fmtPrice(currentPrice) : "-"} + TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit ? " ✅" : ""} + TP2: ${fmtPrice(p.tp2_price)} + SL: ${fmtPrice(p.sl_price)} +
+
+ 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 + 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 + 信号→下单 {p.signal_to_order_ms||0}ms + 下单→成交 {p.order_to_fill_ms||0}ms + #{p.binance_order_id||"-"} +
+
+ ); + })} +
+
+ ); +} + +// ─── 权益曲线 ──────────────────────────────────────────────────── +function EquityCurve() { + const [data, setData] = useState([]); + 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 ( +
+

权益曲线 (累计PnL)

+
+ + + bjt(v)} tick={{ fontSize: 10 }} /> + `${v}R`} /> + bjt(Number(v))} formatter={(v: any) => [`${v}R`, "累计PnL"]} /> + + + + +
+
+ ); +} + +// ─── 历史交易 ──────────────────────────────────────────────────── +type FilterSymbol = "all" | "BTC" | "ETH" | "XRP" | "SOL"; +type FilterResult = "all" | "win" | "loss"; + +function TradeHistory() { + const [trades, setTrades] = useState([]); + const [symbol, setSymbol] = useState("all"); + const [result, setResult] = useState("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 ( +
+
+

历史交易

+
+ {(["all","BTC","ETH","XRP","SOL"] as FilterSymbol[]).map(s => ())} + | + {(["all","win","loss"] as FilterResult[]).map(r => ())} +
+
+
+ {trades.length === 0 ?
暂无交易记录
: ( + + + + + + + + + + + + + + + {trades.map((t: any) => { + const holdMin = t.exit_ts && t.entry_ts ? Math.round((t.exit_ts - t.entry_ts) / 60000) : 0; + return ( + + + + + + + + + + + + + ); + })} + +
币种方向入场成交出场PnL(R)状态滑点费用时间
{t.symbol?.replace("USDT","")}{t.direction === "LONG" ? "🟢" : "🔴"} {t.direction}{fmtPrice(t.entry_price)}{t.fill_price ? fmtPrice(t.fill_price) : "-"}{t.exit_price ? fmtPrice(t.exit_price) : "-"} 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)}{t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status}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${(t.fee_usdt||0).toFixed(2)}{holdMin}m
+ )} +
+
+ ); +} + +// ─── 详细统计 ──────────────────────────────────────────────────── +function StatsPanel() { + const [data, setData] = useState(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 ( +
+

详细统计

+
+
胜率

{data.win_rate}%

+
盈亏比

{data.win_loss_ratio}

+
平均盈利

+{data.avg_win}R

+
平均亏损

-{data.avg_loss}R

+
最大回撤

{data.mdd}R

+
总盈亏

= 0 ? "text-emerald-600" : "text-red-500"}`}>{data.total_pnl >= 0 ? "+" : ""}{data.total_pnl}R

+
总笔数

{data.total}

+
滑点P50

{data.p50_slippage_bps}bps

+
滑点P95

{data.p95_slippage_bps}bps

+
平均滑点

{data.avg_slippage_bps}bps

+
+ {data.by_symbol && ( +
+

按币种

+
+ {Object.entries(data.by_symbol).map(([sym, v]: [string, any]) => ( +
+ {sym.replace("USDT","")} + {v.total}笔 {v.win_rate}% + = 0 ? "text-emerald-600" : "text-red-500"}`}>{v.total_pnl >= 0 ? "+" : ""}{v.total_pnl}R +
+ ))} +
+
+ )} +
+ ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── +export default function LiveTradingPage() { + const { isLoggedIn, loading } = useAuth(); + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看实盘

+ 登录 +
+ ); + + return ( +
+
+

⚡ 实盘交易

+

V5.2策略 · 币安USDT永续合约 · 测试网

+
+ + + + + + + +
+ ); +} \ No newline at end of file diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 457094e..5e8bffc 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -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 ──" },