diff --git a/frontend/app/paper-v53middle/page.tsx b/frontend/app/paper-v53middle/page.tsx new file mode 100644 index 0000000..bda3737 --- /dev/null +++ b/frontend/app/paper-v53middle/page.tsx @@ -0,0 +1,405 @@ +"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; +} + +const STRATEGY = "v53_middle"; +const ALL_COINS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"]; + +// ─── 最新信号 ──────────────────────────────────────────────────── + +function LatestSignals() { + const [signals, setSignals] = useState>({}); + useEffect(() => { + const f = async () => { + for (const sym of ALL_COINS) { + const coin = sym.replace("USDT", ""); + try { + const r = await authFetch(`/api/signals/signal-history?symbol=${coin}&limit=1&strategy=${STRATEGY}`); + if (r.ok) { const j = await r.json(); if (j.data?.length > 0) setSignals(prev => ({ ...prev, [sym]: j.data[0] })); } + } catch {} + } + }; + f(); const iv = setInterval(f, 15000); return () => clearInterval(iv); + }, []); + + return ( +
+
+

最新信号

+ v53 +
+
+ {ALL_COINS.map(sym => { + const s = signals[sym]; + const coin = sym.replace("USDT", ""); + const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null; + const fc = s?.factors; + const gatePassed = fc?.gate_passed ?? true; + return ( +
+
+
+ {coin} + {s?.signal ? ( + <> + + {s.signal === "LONG" ? "🟢" : "🔴"} {s.signal} + + {s.score}分 + + ) : 暂无信号} +
+ {ago !== null && {ago < 60 ? `${ago}m前` : `${Math.round(ago/60)}h前`}} +
+ {fc && ( +
+ + {gatePassed ? "✅" : "❌"} {fc.gate_block || "Gate"} + + 方向{fc.direction?.score ?? 0}/55 + 拥挤{fc.crowding?.score ?? 0}/25 + 环境{fc.environment?.score ?? 0}/15 + 辅助{fc.auxiliary?.score ?? 0}/5 +
+ )} +
+ ); + })} +
+
+ ); +} + +// ─── 控制面板 ──────────────────────────────────────────────────── + +function ControlPanel() { + const [config, setConfig] = useState(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 ( +
+
+ + {config.enabled ? "🟢 运行中" : "⚪ 已停止"} +
+
+ 初始: ${config.initial_balance?.toLocaleString()} + 风险: {(config.risk_per_trade * 100).toFixed(0)}% + 最大: {config.max_positions}仓 +
+
+ ); +} + +// ─── 总览 ──────────────────────────────────────────────────────── + +function SummaryCards() { + const [data, setData] = useState(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); + }, []); + if (!data) return
加载中...
; + return ( +
+ {[ + { 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: "PF", 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: "accumulating", color: "text-slate-600" }, + ].map(({ label, value, sub, color }) => ( +
+

{label}

+

{value}

+

{sub}

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

当前持仓 ● 实时

+
+ {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; + const fc = parseFactors(p.score_factors); + const track = fc?.track||(p.symbol==="BTCUSDT"?"BTC":"ALT"); + return ( +
+
+
+ {p.direction==="LONG"?"🟢":"🔴"} {sym} {p.direction} + {track} + 评分{p.score} +
+
+ =0?"text-emerald-600":"text-red-500"}`}>{unrealR>=0?"+":""}{unrealR.toFixed(2)}R + {holdMin}m +
+
+
+ 入: ${fmtPrice(p.entry_price)} + 现: ${currentPrice?fmtPrice(currentPrice):"-"} + TP1: ${fmtPrice(p.tp1_price)}{p.tp1_hit?" ✅":""} + TP2: ${fmtPrice(p.tp2_price)} + SL: ${fmtPrice(p.sl_price)} +
+
+ 入场时间: {p.entry_ts ? new Date(p.entry_ts).toLocaleString("zh-CN", {hour12:false, month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit", second:"2-digit"} as any) : "-"} +
+
+ ); + })} +
+
+ ); +} + +// ─── 权益曲线 ──────────────────────────────────────────────────── + +function EquityCurve() { + const [data, setData] = useState([]); + 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); + }, []); + return ( +
+

权益曲线

+ {data.length < 2 ?
数据积累中...
: ( +
+ + + 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/paper/trades?symbol=${symbol}&result=${result}&strategy=${STRATEGY}&limit=50`); if (r.ok) setTrades((await r.json()).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; + const fc = parseFactors(t.score_factors); + const track = fc?.track||(t.symbol==="BTCUSDT"?"BTC":"ALT"); + const fmtTime = (ms: number) => ms ? new Date(ms).toLocaleString("zh-CN", {hour12:false, month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit"} as any) : "-"; + return ( + + + + + + + + + + + + + ); + })} + +
币种方向入场价出场价PnL(R)状态分数入场时间出场时间持仓
{t.symbol?.replace("USDT","")}{track}{t.direction==="LONG"?"🟢":"🔴"} {t.direction}{fmtPrice(t.entry_price)}{t.exit_price?fmtPrice(t.exit_price):"-"}0?"text-emerald-600":t.pnl_r<0?"text-red-500":"text-slate-500"}`}>{t.pnl_r>0?"+":""}{t.pnl_r?.toFixed(2)}{t.status==="tp"?"止盈":t.status==="sl"?"止损":t.status==="sl_be"?"保本":t.status==="timeout"?"超时":t.status==="signal_flip"?"翻转":t.status}{t.score}{fmtTime(t.entry_ts)}{fmtTime(t.exit_ts)}{holdMin}m
+ )} +
+
+ ); +} + +// ─── 统计面板 ──────────────────────────────────────────────────── + +function StatsPanel() { + const [data, setData] = useState(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); + }, []); + if (!data || data.error) return ( +
+

详细统计

+
等待交易记录积累...
+
+ ); + const coinTabs = ["ALL","BTC","ETH","XRP","SOL"]; + const st = tab==="ALL"?data:(data.by_symbol?.[tab]||null); + return ( +
+
+

详细统计

+
+ {coinTabs.map(t => ( + + ))} +
+
+ {st ? ( +
+
+
胜率

{st.win_rate}%

+
盈亏比

{st.win_loss_ratio}

+
平均盈利

+{st.avg_win}R

+
平均亏损

-{st.avg_loss}R

+
最大回撤

{st.mdd}R

+
夏普比率

{st.sharpe}

+
总盈亏

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

+
总笔数

{st.total??data.total}

+
做多胜率

{st.long_win_rate}% ({st.long_count}笔)

+
做空胜率

{st.short_win_rate}% ({st.short_count}笔)

+
+
+ ) :
该币种暂无数据
} +
+ ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── + +export default function PaperTradingV53Page() { + const { isLoggedIn, loading } = useAuth(); + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看模拟盘

+ 登录 +
+ ); + return ( +
+ {/* Fast 实验版标识条 */} +
+ 🚀 V5.3 Middle — 实验变体 A/B对照 +
+ CVD 5m/30m + OBI+加分 + accel独立触发 +
+
+
+

🚀 模拟盘 V5.3 Middle

+

实验变体 v53_middle · BTC/ETH/XRP/SOL · CVD 5m/30m · OBI正向加分 · 与 V5.3 同起点对照

+
+ + + + + + + +
+ ); +} diff --git a/frontend/app/signals-v53middle/page.tsx b/frontend/app/signals-v53middle/page.tsx new file mode 100644 index 0000000..fabd3b9 --- /dev/null +++ b/frontend/app/signals-v53middle/page.tsx @@ -0,0 +1,580 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { authFetch } from "@/lib/auth"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; +import { + ComposedChart, Area, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, + ReferenceLine, CartesianGrid, Legend +} from "recharts"; + +type Symbol = "BTC" | "ETH" | "XRP" | "SOL"; + +interface IndicatorRow { + ts: number; + cvd_fast: number; + cvd_mid: number; + cvd_day: number; + atr_5m: number; + vwap_30m: number; + price: number; + score: number; + signal: string | null; +} + +interface LatestIndicator { + ts: number; + cvd_fast: number; + cvd_mid: number; + cvd_day: number; + cvd_fast_slope: number; + atr_5m: number; + atr_percentile: number; + vwap_30m: number; + price: number; + p95_qty: number; + p99_qty: number; + score: number; + display_score?: number; // v53_btc: alt_score_ref(参考分) + gate_passed?: boolean; // v53_btc顶层字段 + signal: string | null; + tier?: "light" | "standard" | "heavy" | null; + factors?: { + track?: string; + direction?: { score?: number; max?: number; cvd_resonance?: number; p99_flow?: number; accel_bonus?: number; accel_independent_score?: number }; + crowding?: { score?: number; max?: number; lsr_contrarian?: number; top_trader_position?: number }; + environment?: { score?: number; max?: number; obi_bonus?: number; oi_base?: number }; + auxiliary?: { score?: number; max?: number; coinbase_premium?: number }; + // BTC gate fields + gate_passed?: boolean; + block_reason?: string; // BTC用 + gate_block?: string; // ALT用 + obi_raw?: number; + spot_perp_div?: number; + whale_cvd_ratio?: number; + atr_pct_price?: number; + alt_score_ref?: number; + } | null; +} + +const WINDOWS = [ + { label: "1h", value: 60 }, + { label: "4h", value: 240 }, + { label: "12h", value: 720 }, + { label: "24h", value: 1440 }, +]; + +function bjtStr(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; +} + +function bjtFull(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")}:${String(d.getUTCSeconds()).padStart(2, "0")}`; +} + +function fmt(v: number, decimals = 1): string { + if (Math.abs(v) >= 1000000) return `${(v / 1000000).toFixed(1)}M`; + if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(1)}K`; + return v.toFixed(decimals); +} + +function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) { + const ratio = Math.max(0, Math.min((score / max) * 100, 100)); + return ( +
+ {label} +
+
+
+ {score}/{max} +
+ ); +} + +// ─── ALT Gate 状态卡片 ────────────────────────────────────────── + +const ALT_GATE_THRESHOLDS: Record = { + ETH: { vol: "0.3%", obi: "0.35", spd: "0.5%", whale: "$50k" }, + XRP: { vol: "0.25%", obi: "0.40", spd: "0.6%", whale: "$30k" }, + SOL: { vol: "0.4%", obi: "0.45", spd: "0.8%", whale: "$20k" }, +}; + +function ALTGateCard({ symbol, factors }: { symbol: Symbol; factors: LatestIndicator["factors"] }) { + if (!factors || symbol === "BTC") return null; + const thresholds = ALT_GATE_THRESHOLDS[symbol] ?? ALT_GATE_THRESHOLDS["ETH"]; + const passed = factors.gate_passed ?? true; + const blockReason = factors.gate_block; + return ( +
+
+

🔒 {symbol} Gate-Control

+ + {passed ? "✅ Gate通过" : "❌ 否决"} + +
+
+
+

波动率

+

{((factors.atr_pct_price ?? 0) * 100).toFixed(3)}%

+

需 ≥{thresholds.vol}

+
+
+

OBI

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {((factors.obi_raw ?? 0) * 100).toFixed(2)}% +

+

否决±{thresholds.obi}

+
+
+

期现背离

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {((factors.spot_perp_div ?? 0) * 10000).toFixed(2)}bps +

+

否决±{thresholds.spd}

+
+
+

鲸鱼阈值

+

{thresholds.whale}

+

大单门槛

+
+
+ {blockReason && ( +

+ 否决原因: {blockReason} +

+ )} +
+ ); +} + +// ─── BTC Gate 状态卡片 ─────────────────────────────────────────── + +function BTCGateCard({ factors }: { factors: LatestIndicator["factors"] }) { + if (!factors) return null; + return ( +
+
+

⚡ BTC Gate-Control

+ + {factors.gate_passed ? "✅ Gate通过" : "❌ 否决"} + +
+
+
+

波动率

+

{((factors.atr_pct_price ?? 0) * 100).toFixed(3)}%

+

需 ≥0.2%

+
+
+

OBI

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {((factors.obi_raw ?? 0) * 100).toFixed(2)}% +

+

盘口失衡

+
+
+

期现背离

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {((factors.spot_perp_div ?? 0) * 10000).toFixed(2)}bps +

+

spot-perp

+
+
+

巨鲸CVD

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {((factors.whale_cvd_ratio ?? 0) * 100).toFixed(2)}% +

+

>$100k

+
+
+ {factors.block_reason && ( +

+ 否决原因: {factors.block_reason} +

+ )} +
+ ); +} + +// ─── 实时指标卡片 ──────────────────────────────────────────────── + +function IndicatorCards({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState(null); + const strategy = "v53_middle"; + + useEffect(() => { + const fetch = async () => { + try { + const res = await authFetch(`/api/signals/latest?strategy=${strategy}`); + if (!res.ok) return; + const json = await res.json(); + setData(json[symbol] || null); + } catch {} + }; + fetch(); + const iv = setInterval(fetch, 5000); + return () => clearInterval(iv); + }, [symbol, strategy]); + + if (!data) return
等待指标数据...
; + + const isBTC = symbol === "BTC"; + const priceVsVwap = data.price > data.vwap_30m ? "上方" : "下方"; + + return ( +
+ {/* CVD三轨 */} +
+
+

CVD_fast (5m实算★)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {fmt(data.cvd_fast)} +

+

+ 斜率: = 0 ? "text-emerald-600" : "text-red-500"}> + {data.cvd_fast_slope >= 0 ? "↑" : "↓"}{fmt(Math.abs(data.cvd_fast_slope))} + +

+
+
+

CVD_mid (30m实算★)

+

= 0 ? "text-emerald-600" : "text-red-500"}`}> + {fmt(data.cvd_mid)} +

+

{data.cvd_mid > 0 ? "多" : "空"}头占优

+
+
+

CVD共振

+

= 0 && data.cvd_mid >= 0 ? "text-emerald-600" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "text-red-500" : "text-slate-400"}`}> + {data.cvd_fast >= 0 && data.cvd_mid >= 0 ? "✅ 多头共振" : data.cvd_fast < 0 && data.cvd_mid < 0 ? "✅ 空头共振" : "⚠️ 分歧"} +

+

V5.3核心信号

+
+
+ + {/* ATR + VWAP */} +
+
+

ATR

+

${fmt(data.atr_5m, 2)}

+

+ 60 ? "text-amber-600 font-semibold" : "text-slate-400"}> + {data.atr_percentile.toFixed(0)}%{data.atr_percentile > 60 ? "🔥" : ""} + +

+
+
+

VWAP

+

${data.vwap_30m.toLocaleString("en-US", { maximumFractionDigits: 1 })}

+

+ 价格在 data.vwap_30m ? "text-emerald-600" : "text-red-500"}>{priceVsVwap} +

+
+
+

P95

+

{data.p95_qty.toFixed(4)}

+

大单阈值

+
+
+

P99

+

{data.p99_qty.toFixed(4)}

+

超大单

+
+
+ + {/* 信号状态 */} +
+
+
+

+ {isBTC ? "BTC Gate-Control" : "ALT 四层评分"} + {" · "}{"v53"} +

+

+ {data.signal === "LONG" ? "🟢 做多" : data.signal === "SHORT" ? "🔴 做空" : "⚪ 无信号"} +

+
+
+ {isBTC ? ( + <> +

+ {data.display_score ?? data.factors?.alt_score_ref ?? data.score}/100 + 参考分 +

+

+ {(data.gate_passed ?? data.factors?.gate_passed) ? (data.tier === "standard" ? "标准" : "不开仓") : "Gate否决"} +

+ + ) : ( + <> +

{data.score}/100

+

{data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : "不开仓"}

+ + )} +
+
+ + {/* 四层分数 — ALT和BTC都显示 */} +
+ + {data.factors?.direction?.accel_independent_score != null && data.factors.direction.accel_independent_score > 0 && ( +

⚡ accel独立触发 +{data.factors.direction.accel_independent_score}

+ )} + + + {(data.factors?.environment?.obi_bonus ?? 0) > 0 && ( +

📊 OBI正向 +{data.factors?.environment?.obi_bonus}

+ )} + +
+
+ + {/* ALT Gate 卡片 */} + {!isBTC && data.factors && } + + {/* BTC Gate 卡片 */} + {isBTC && data.factors && } +
+ ); +} + +// ─── 信号历史 ──────────────────────────────────────────────────── + +interface SignalRecord { + ts: number; + score: number; + signal: string; +} + +function SignalHistory({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState([]); + const strategy = "v53_middle"; + + useEffect(() => { + const fetchData = async () => { + try { + const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20&strategy=${strategy}`); + if (!res.ok) return; + const json = await res.json(); + setData(json.data || []); + } catch {} + }; + fetchData(); + const iv = setInterval(fetchData, 15000); + return () => clearInterval(iv); + }, [symbol, strategy]); + + if (data.length === 0) return null; + + return ( +
+
+

最近信号 ({strategy})

+
+
+ {data.map((s, i) => ( +
+
+ + {s.signal === "LONG" ? "🟢 LONG" : "🔴 SHORT"} + + {bjtFull(s.ts)} +
+
+ {s.score} + = 85 ? "bg-red-100 text-red-700" : + s.score >= 75 ? "bg-blue-100 text-blue-700" : + "bg-slate-100 text-slate-600" + }`}> + {s.score >= 85 ? "加仓" : s.score >= 75 ? "标准" : "不开仓"} + +
+
+ ))} +
+
+ ); +} + +// ─── CVD图表 ──────────────────────────────────────────────────── + +function CVDChart({ symbol, minutes }: { symbol: Symbol; minutes: number }) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const strategy = "v53_middle"; + + const fetchData = useCallback(async (silent = false) => { + try { + const res = await authFetch(`/api/signals/indicators?symbol=${symbol}&minutes=${minutes}&strategy=${strategy}`); + if (!res.ok) return; + const json = await res.json(); + setData(json.data || []); + if (!silent) setLoading(false); + } catch {} + }, [symbol, minutes, strategy]); + + useEffect(() => { + setLoading(true); + fetchData(); + const iv = setInterval(() => fetchData(true), 30000); + return () => clearInterval(iv); + }, [fetchData]); + + const chartData = data.map(d => ({ + time: bjtStr(d.ts), + fast: parseFloat(d.cvd_fast?.toFixed(2) || "0"), + mid: parseFloat(d.cvd_mid?.toFixed(2) || "0"), + price: d.price, + })); + + const prices = chartData.map(d => d.price).filter(v => v > 0); + const pMin = prices.length ? Math.min(...prices) : 0; + const pMax = prices.length ? Math.max(...prices) : 0; + const pPad = (pMax - pMin) * 0.3 || pMax * 0.001; + + if (loading) return
加载指标数据...
; + if (data.length === 0) return
暂无 V5.3 指标数据,signal-engine 需运行积累
; + + return ( + + + + + + v >= 1000 ? `$${(v / 1000).toFixed(1)}k` : `$${v.toFixed(0)}`} + /> + { + if (name === "price") return [`$${Number(v).toLocaleString()}`, "币价"]; + if (name === "fast") return [fmt(Number(v)), "CVD_fast(5m实算)"]; + return [fmt(Number(v)), "CVD_mid(30m实算)"]; + }} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} + /> + + + + + + + + ); +} + +// ─── 主页面 ────────────────────────────────────────────────────── + +export default function SignalsV53Page() { + const { isLoggedIn, loading } = useAuth(); + const [symbol, setSymbol] = useState("ETH"); + const [minutes, setMinutes] = useState(240); + + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看信号数据

+
+ 登录 + 注册 +
+
+ ); + + return ( +
+ {/* Fast 实验版标识条 */} +
+ 🚀 V5.3 Middle — 实验变体 A/B对照 +
+ CVD 5m/30m + OBI+加分 + accel独立触发 +
+
+
+
+

🚀 信号引擎 V5.3 Middle

+

+ CVD 5m/30m · OBI正向加分 · accel独立触发 · 实验变体 · + {symbol === "BTC" ? " 🔵 BTC轨(gate-control)" : " 🟣 ALT轨(ETH/XRP/SOL)"} +

+
+
+ {(["BTC", "ETH", "XRP", "SOL"] as Symbol[]).map(s => ( + + ))} +
+
+ + + + +
+
+
+

CVD三轨 + 币价

+

蓝=fast(DB存30m,实算5m★) · 紫=mid(DB存4h,实算30m★) · 橙=价格

+
+
+ {WINDOWS.map(w => ( + + ))} +
+
+
+ +
+
+ +
+
+

📖 V5.3 双轨信号说明

+
+
+
+ 🟣 ALT轨(ETH/XRP/SOL)— 四层线性评分 +
+

1️⃣ 方向层(55分) — CVD共振30分(fast+mid同向)+ P99大单对齐20分 + 加速奖励5分。删除独立确认层,解决CVD双重计分问题。

+

2️⃣ 拥挤层(25分) — LSR反向拥挤15分(散户过度拥挤=信号)+ 大户持仓方向10分。

+

3️⃣ 环境层(15分) — OI变化率,新资金进场vs撤离,判断趋势持续性。

+

4️⃣ 辅助层(5分) — Coinbase Premium,美系机构动向。

+
+
+
+ 🔵 BTC轨 — Gate-Control逻辑 +
+

波动率门控:ATR/Price ≥ 0.2%,低波动行情拒绝开仓

+

OBI否决:订单簿失衡超阈值且与信号方向冲突时否决(实时100ms)

+

期现背离否决:spot与perp价差超阈值时否决(实时1s)

+

巨鲸CVD:>$100k成交额净CVD,15分钟滚动窗口实时计算

+
+
+
+ 档位:<75不开仓 · 75-84标准 · ≥85加仓 · 冷却10分钟 +
+
+
+
+ ); +} diff --git a/frontend/app/strategy-plaza/[id]/page.tsx b/frontend/app/strategy-plaza/[id]/page.tsx index 7a00198..024ec7d 100644 --- a/frontend/app/strategy-plaza/[id]/page.tsx +++ b/frontend/app/strategy-plaza/[id]/page.tsx @@ -1,21 +1,27 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; import { useParams, useSearchParams, useRouter } from "next/navigation"; +import { useEffect, useState, useCallback } from "react"; import { authFetch } from "@/lib/auth"; import Link from "next/link"; +import dynamic from "next/dynamic"; import { ArrowLeft, CheckCircle, PauseCircle, AlertCircle, Clock, - TrendingUp, - TrendingDown, } from "lucide-react"; -// ─── Types ──────────────────────────────────────────────────────── +// ─── Dynamic imports for each strategy's pages ─────────────────── +const SignalsV53 = dynamic(() => import("@/app/signals-v53/page"), { ssr: false }); +const SignalsV53Fast = dynamic(() => import("@/app/signals-v53fast/page"), { ssr: false }); +const SignalsV53Middle = dynamic(() => import("@/app/signals-v53middle/page"), { ssr: false }); +const PaperV53 = dynamic(() => import("@/app/paper-v53/page"), { ssr: false }); +const PaperV53Fast = dynamic(() => import("@/app/paper-v53fast/page"), { ssr: false }); +const PaperV53Middle = dynamic(() => import("@/app/paper-v53middle/page"), { ssr: false }); +// ─── Types ──────────────────────────────────────────────────────── interface StrategySummary { id: string; display_name: string; @@ -32,51 +38,17 @@ interface StrategySummary { open_positions: number; pnl_usdt_24h: number; pnl_r_24h: number; - std_r: number; cvd_windows?: string; description?: string; } -interface Signal { - ts: number; - symbol: string; - score: number; - signal: string | null; - price: number; - factors: any; -} - -interface Trade { - id: number; - symbol: string; - direction: string; - score: number; - entry_price: number; - exit_price: number | null; - tp1_price: number; - tp2_price: number; - sl_price: number; - tp1_hit: boolean; - pnl_r: number; - risk_distance: number; - entry_ts: number; - exit_ts: number | null; - status: string; -} - // ─── Helpers ────────────────────────────────────────────────────── - -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 fmtDur(ms: number) { const s = Math.floor((Date.now() - ms) / 1000); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); - if (d > 0) return `${d}天${h}h`; const m = Math.floor((s % 3600) / 60); + if (d > 0) return `${d}天${h}h`; if (h > 0) return `${h}h${m}m`; return `${m}m`; } @@ -87,143 +59,22 @@ function StatusBadge({ status }: { status: string }) { return 异常; } -// ─── Sub-views ──────────────────────────────────────────────────── - -function SignalsView({ strategyId }: { strategyId: string }) { - const [signals, setSignals] = useState([]); - const [loading, setLoading] = useState(true); - - const fetch_ = useCallback(async () => { - try { - const r = await authFetch(`/api/strategy-plaza/${strategyId}/signals?limit=40`); - const d = await r.json(); - setSignals(d.signals || []); - } catch {} - setLoading(false); - }, [strategyId]); - - useEffect(() => { fetch_(); const iv = setInterval(fetch_, 15000); return () => clearInterval(iv); }, [fetch_]); - - if (loading) return
加载信号中...
; - - return ( -
- - - - - - - - - - - - - - {signals.map((s, i) => { - const fc = s.factors && (typeof s.factors === "string" ? JSON.parse(s.factors) : s.factors); - return ( - - - - - - - - - - ); - })} - -
时间(北京)币种价格分数信号CVD 30mCVD 4h
{bjt(s.ts)}{s.symbol.replace("USDT", "")}{s.price?.toLocaleString()} - = 75 ? "text-emerald-400" : s.score >= 50 ? "text-yellow-400" : "text-gray-500"}`}> - {s.score} - - - {s.signal ? ( - - {s.signal} - - ) : } - {fc?.cvd_30m?.toFixed(0) ?? "—"}{fc?.cvd_4h?.toFixed(0) ?? "—"}
- {signals.length === 0 &&
暂无信号数据
} -
- ); +// ─── Content router ─────────────────────────────────────────────── +function SignalsContent({ strategyId }: { strategyId: string }) { + if (strategyId === "v53") return ; + if (strategyId === "v53_fast") return ; + if (strategyId === "v53_middle") return ; + return
未知策略: {strategyId}
; } -function TradesView({ strategyId }: { strategyId: string }) { - const [trades, setTrades] = useState([]); - const [loading, setLoading] = useState(true); - - const fetch_ = useCallback(async () => { - try { - const r = await authFetch(`/api/strategy-plaza/${strategyId}/trades?limit=50`); - const d = await r.json(); - setTrades(d.trades || []); - } catch {} - setLoading(false); - }, [strategyId]); - - useEffect(() => { fetch_(); }, [fetch_]); - - if (loading) return
加载交易记录中...
; - - return ( -
- - - - - - - - - - - - - - - - {trades.map((t) => { - const isWin = t.pnl_r > 0; - const isActive = !t.exit_ts; - return ( - - - - - - - - - - - - ); - })} - -
入场时间出场时间币种方向入场价出场价盈亏R盈亏U状态
{bjt(t.entry_ts)}{t.exit_ts ? bjt(t.exit_ts) : 持仓中}{t.symbol.replace("USDT", "")} - - {t.direction === "LONG" ? "多" : "空"} - - {t.entry_price?.toLocaleString()}{t.exit_price?.toLocaleString() ?? "—"} - {isActive ? "活跃" : `${isWin ? "+" : ""}${t.pnl_r?.toFixed(3)}R`} - - {isActive ? "—" : `${isWin ? "+" : ""}${Math.round(t.pnl_r * 200)}U`} - - - {isActive ? "活跃" : isWin ? "盈利" : "亏损"} - -
- {trades.length === 0 &&
暂无交易记录
} -
- ); +function PaperContent({ strategyId }: { strategyId: string }) { + if (strategyId === "v53") return ; + if (strategyId === "v53_fast") return ; + if (strategyId === "v53_middle") return ; + return
未知策略: {strategyId}
; } // ─── Main Page ──────────────────────────────────────────────────── - export default function StrategyDetailPage() { const params = useParams(); const searchParams = useSearchParams(); @@ -237,90 +88,87 @@ export default function StrategyDetailPage() { const fetchSummary = useCallback(async () => { try { const r = await authFetch(`/api/strategy-plaza/${strategyId}/summary`); - if (r.ok) { const d = await r.json(); setSummary(d); } + if (r.ok) { + const d = await r.json(); + setSummary(d); + } } catch {} setLoading(false); }, [strategyId]); - useEffect(() => { fetchSummary(); const iv = setInterval(fetchSummary, 30000); return () => clearInterval(iv); }, [fetchSummary]); + useEffect(() => { + fetchSummary(); + const iv = setInterval(fetchSummary, 30000); + return () => clearInterval(iv); + }, [fetchSummary]); - if (loading) return
加载中...
; - if (!summary) return
策略不存在
; + if (loading) { + return ( +
+
加载中...
+
+ ); + } - const isProfit = summary.net_usdt >= 0; + const isProfit = (summary?.net_usdt ?? 0) >= 0; return ( -
- {/* Back */} - - - 返回策略广场 - - - {/* Header */} -
-
-
-

{summary.display_name}

- {summary.description &&

{summary.description}

} -
- - - - 运行 {fmtDur(summary.started_at)} - - {summary.cvd_windows && ( - CVD {summary.cvd_windows} - )} -
-
-
-
- {isProfit ? "+" : ""}{summary.net_usdt.toLocaleString()} U -
-
{summary.current_balance.toLocaleString()} / {summary.initial_balance.toLocaleString()} USDT
-
-
- - {/* Stats */} -
- {[ - { label: "胜率", value: `${summary.win_rate}%`, color: summary.win_rate >= 50 ? "text-emerald-400" : summary.win_rate >= 45 ? "text-yellow-400" : "text-red-400" }, - { label: "净R", value: `${summary.net_r >= 0 ? "+" : ""}${summary.net_r}R`, color: summary.net_r >= 0 ? "text-emerald-400" : "text-red-400" }, - { label: "24h盈亏", value: `${summary.pnl_usdt_24h >= 0 ? "+" : ""}${summary.pnl_usdt_24h}U`, color: summary.pnl_usdt_24h >= 0 ? "text-emerald-400" : "text-red-400" }, - { label: "总交易数", value: summary.trade_count, color: "text-white" }, - ].map(({ label, value, color }) => ( -
-
{label}
-
{value}
-
- ))} -
+
+ {/* Back + Strategy Header */} +
+ + + 策略广场 + + / + {summary?.display_name ?? strategyId}
+ {/* Summary Bar */} + {summary && ( +
+ + + 运行 {fmtDur(summary.started_at)} + + {summary.cvd_windows && ( + CVD {summary.cvd_windows} + )} + + 胜率 = 50 ? "text-emerald-400 font-bold" : "text-yellow-400 font-bold"}>{summary.win_rate}% + 净R {summary.net_r >= 0 ? "+" : ""}{summary.net_r}R + 余额 {summary.current_balance.toLocaleString()} U + 24h = 0 ? "text-emerald-400" : "text-red-400"}`}>{summary.pnl_usdt_24h >= 0 ? "+" : ""}{summary.pnl_usdt_24h} U + +
+ )} + {/* Tabs */} -
- {["signals", "paper"].map((t) => ( +
+ {[ + { key: "signals", label: "📊 信号引擎" }, + { key: "paper", label: "📈 模拟盘" }, + ].map(({ key, label }) => ( ))}
- {/* Tab Content */} -
+ {/* Content — direct render of existing pages */} +
{tab === "signals" ? ( - + ) : ( - + )}