diff --git a/frontend/app/signals/page.tsx b/frontend/app/signals/page.tsx index a4249fb..978c0d2 100644 --- a/frontend/app/signals/page.tsx +++ b/frontend/app/signals/page.tsx @@ -1,60 +1,321 @@ "use client"; -import { useEffect, useState } from "react"; -import { api, SignalHistoryItem } from "@/lib/api"; +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"; -export default function SignalsPage() { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); +type Symbol = "BTC" | "ETH"; + +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; + signal: string | 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 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 IndicatorCards({ symbol }: { symbol: Symbol }) { + const [data, setData] = useState(null); useEffect(() => { - const run = async () => { + const fetch = async () => { try { - const data = await api.signalsHistory(); - setItems(data.items || []); - } catch { - setError("加载信号历史失败"); - } finally { - setLoading(false); - } + const res = await authFetch("/api/signals/latest"); + if (!res.ok) return; + const json = await res.json(); + setData(json[symbol] || null); + } catch {} }; - run(); - }, []); + fetch(); + const iv = setInterval(fetch, 5000); + return () => clearInterval(iv); + }, [symbol]); + + if (!data) return
等待指标数据...
; + + const cvdFastDir = data.cvd_fast > 0 ? "多" : "空"; + const cvdMidDir = data.cvd_mid > 0 ? "多" : "空"; + const priceVsVwap = data.price > data.vwap_30m ? "上方" : "下方"; + + // 核心条件检查 + const core1 = data.cvd_fast > 0 && data.cvd_fast_slope > 0 ? "✅" : data.cvd_fast < 0 && data.cvd_fast_slope < 0 ? "✅空" : "⬜"; + const core2 = data.cvd_mid !== 0 ? "✅" : "⬜"; + const core3 = data.price !== data.vwap_30m ? "✅" : "⬜"; return ( -
-

信号历史

-
- {loading ?

加载中...

: null} - {error ?

{error}

: null} - {!loading && !error ? ( - - - - - - - - - - - {items.map((row) => ( - - - - - - - ))} - {!items.length ? ( - - - - ) : null} - -
时间币种年化信号类型
{new Date(row.sent_at).toLocaleString("zh-CN")}{row.symbol}{row.annualized}%资金费率套利信号
暂无信号记录(费率超10%时自动触发)
- ) : null} +
+ {/* CVD三轨 */} +
+
+

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)} +

+

大方向: {cvdMidDir}头占优

+
+
+

CVD_day (日内)

+

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

+

盘中基线

+
+
+ + {/* ATR + VWAP + 大单 */} +
+
+

ATR (5m×14)

+

${fmt(data.atr_5m, 2)}

+

+ 分位: 60 ? "text-amber-600 font-semibold" : "text-slate-500"}> + {data.atr_percentile.toFixed(0)}% + + {data.atr_percentile > 60 ? " 🔥扩张" : data.atr_percentile < 40 ? " 压缩" : ""} +

+
+
+

VWAP (30m)

+

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

+

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

+
+
+

大单阈值 P95

+

{data.p95_qty.toFixed(4)}

+

24h动态分位

+
+
+

大单阈值 P99

+

{data.p99_qty.toFixed(4)}

+

超大单门槛

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

当前信号

+

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

+
+
+

加分

+

{data.score}/60

+
+
+ {data.signal && ( +
+ {core1} CVD_fast方向 + {core2} CVD_mid方向 + {core3} VWAP位置 +
+ )} +
+
+ ); +} + +// ─── CVD三轨图 ────────────────────────────────────────────────── + +function CVDChart({ symbol, minutes }: { symbol: Symbol; minutes: number }) { + 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}`); + if (!res.ok) return; + const json = await res.json(); + setData(json.data || []); + if (!silent) setLoading(false); + } catch {} + }, [symbol, minutes]); + + 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
暂无指标数据,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 SignalsPage() { + const { isLoggedIn, loading } = useAuth(); + const [symbol, setSymbol] = useState("BTC"); + const [minutes, setMinutes] = useState(240); + + if (loading) return
加载中...
; + if (!isLoggedIn) return ( +
+
🔒
+

请先登录查看信号数据

+
+ 登录 + 注册 +
+
+ ); + + return ( +
+ {/* 标题 */} +
+
+

V5 信号引擎

+

CVD三轨 + ATR + VWAP + 大单阈值 → 做多/做空信号

+
+
+ {(["BTC", "ETH"] as Symbol[]).map(s => ( + + ))} +
+
+ + {/* 实时指标卡片 */} + + + {/* CVD三轨图 */} +
+
+
+

CVD三轨 + 币价

+

蓝色面积=CVD_fast(30m) · 紫色虚线=CVD_mid(4h) · 橙色虚线=币价

+
+
+ {WINDOWS.map(w => ( + + ))} +
+
+
+ +
+
+ + {/* 说明 */} +
+

信号逻辑:CVD_fast方向 + CVD_mid方向 + VWAP位置 = 核心3条件。加分:ATR扩张(+25) + 无反向大单(+20) + 资金费率(+15)。

+

仓位:0-15分→2%仓 / 20-40分→5%仓 / 45-60分→8%仓。冷却10分钟,时间止损30分钟。

); diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index ec0b243..69beb7e 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 + ChevronLeft, ChevronRight, Activity, LogOut, Crosshair } from "lucide-react"; const navItems = [ { href: "/", label: "仪表盘", icon: LayoutDashboard }, { href: "/trades", label: "成交流", icon: Activity }, + { href: "/signals", label: "信号引擎", icon: Crosshair }, { href: "/about", label: "说明", icon: Info }, ];