diff --git a/docs/arbitrage-engine/execution-state-machine.md b/docs/arbitrage-engine/execution-state-machine.md index 90aa513..233f79b 100644 --- a/docs/arbitrage-engine/execution-state-machine.md +++ b/docs/arbitrage-engine/execution-state-machine.md @@ -29,7 +29,7 @@ ### 1.3 状态机伪代码 -```pseudo +```text state = IDLE on_signal_open(signal): @@ -106,7 +106,7 @@ on_timer(): ### 2.3 TP 状态机(maker 主路径 + taker 兜底) -```pseudo +```text on_position_open(pos): // 开仓后立即挂 TP1 限价单(maker) tp1_price = pos.entry_price + pos.side * tp1_r * pos.risk_distance @@ -159,7 +159,7 @@ on_timer(): ### 2.4 SL 状态机(纯 taker) -```pseudo +```text on_sl_trigger(pos, sl_price): // 触发条件可以来自价格监控或止损订单触发 // 这里策略层只关心:一旦触发,立即使用 taker @@ -173,7 +173,7 @@ SL 不做 maker 逻辑,避免在极端行情下挂单无法成交。 ### 2.5 Flip 状态机(平旧仓 + 新开仓) -```pseudo +```text on_flip_signal(pos, new_side, flip_context): if not flip_condition_met(flip_context): return @@ -197,7 +197,7 @@ flip 的关键是:**门槛更高**(如 score < 85 且 OBI 翻转且价格跌 ### 2.6 Timeout 状态机(超时出场) -```pseudo +```text on_timer(): if pos.state == POSITION_OPEN and now() - pos.open_ts >= timeout_seconds: // 可以偏 maker:先挂限价平仓,超时再 taker diff --git a/frontend/components/SignalsView.tsx b/frontend/components/SignalsView.tsx new file mode 100644 index 0000000..893fb43 --- /dev/null +++ b/frontend/components/SignalsView.tsx @@ -0,0 +1,541 @@ +"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; + gate_passed?: boolean; + 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 }; + crowding?: { score?: number; max?: number; lsr_contrarian?: number; top_trader_position?: number }; + environment?: { score?: number; max?: number }; + auxiliary?: { score?: number; max?: number; coinbase_premium?: number }; + gate_passed?: boolean; + block_reason?: string; + gate_block?: string; + 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} +
+ ); +} + +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} +

+ )} +
+ ); +} + +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, strategy }: { symbol: Symbol; strategy: string }) { + const [data, setData] = useState(null); + + 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_fast (30m)

+

= 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 (4h)

+

= 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

+

${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" ? "标准" : "不开仓"}

+ + )} +
+
+ +
+ + + + +
+
+ + {!isBTC && data.factors && } + {isBTC && data.factors && } +
+ ); +} + +interface SignalRecord { + ts: number; + score: number; + signal: string; +} + +function SignalHistory({ symbol, strategy }: { symbol: Symbol; strategy: string }) { + const [data, setData] = useState([]); + + 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 ? "标准" : "不开仓"} + +
+
+ ))} +
+
+ ); +} + +function CVDChart({ symbol, minutes, strategy }: { symbol: Symbol; minutes: number; strategy: string }) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + 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(30m)"]; + return [fmt(Number(v)), "CVD_mid(4h)"]; + }} + contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 11 }} + /> + + + + + + + + ); +} + +export default function SignalsView({ strategy }: { strategy: string }) { + const { isLoggedIn, loading } = useAuth(); + const [symbol, setSymbol] = useState("ETH"); + const [minutes, setMinutes] = useState(240); + + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看信号数据

+
+ 登录 + 注册 +
+
+ ); + + return ( +
+
+
+

⚡ 信号引擎 V5.3

+

+ 四层评分 55/25/15/5 · ALT双轨 + BTC gate-control · + {symbol === "BTC" ? " 🔵 BTC轨(gate-control)" : " 🟣 ALT轨(ETH/XRP/SOL)"} +

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

CVD三轨 + 币价

+

蓝=fast(30m) · 紫=mid(4h) · 橙=价格

+
+
+ {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分钟 +
+
+
+
+ ); +}