"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";
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;
tier?: "light" | "standard" | "heavy" | null;
factors?: {
direction?: { score?: number };
crowding?: { score?: number };
environment?: { score?: number };
confirmation?: { score?: number };
auxiliary?: { score?: number };
} | null;
}
interface MarketIndicatorValue {
value: number;
ts: number;
}
interface MarketIndicatorSet {
long_short_ratio?: MarketIndicatorValue;
top_trader_position?: MarketIndicatorValue;
open_interest_hist?: MarketIndicatorValue;
coinbase_premium?: MarketIndicatorValue;
}
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 pct(v: number, digits = 1): string {
return `${(v * 100).toFixed(digits)}%`;
}
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 (
);
}
function MarketIndicatorsCards({ symbol }: { symbol: Symbol }) {
const [data, setData] = useState(null);
useEffect(() => {
const fetch = async () => {
try {
const res = await authFetch("/api/signals/market-indicators");
if (!res.ok) return;
const json = await res.json();
setData(json[symbol] || null);
} catch {}
};
fetch();
const iv = setInterval(fetch, 5000);
return () => clearInterval(iv);
}, [symbol]);
if (!data) return 等待市场指标数据...
;
const ls = Number(data.long_short_ratio?.value ?? 1);
const top = Number(data.top_trader_position?.value ?? 0.5);
const oi = Number(data.open_interest_hist?.value ?? 0);
const premium = Number(data.coinbase_premium?.value ?? 0);
return (
多空比 (L/S)
Long: {(ls / (1 + ls) * 100).toFixed(1)}%
Short: {(100 - (ls / (1 + ls) * 100)).toFixed(1)}%
大户持仓
大户做多: {(top * 100).toFixed(1)}%
方向: {top >= 0.55 ? "多头占优" : top <= 0.45 ? "空头占优" : "中性"}
OI变化
{pct(oi, 2)}
活跃度: {oi >= 0.03 ? "高" : oi > 0 ? "中" : "低"}
Coinbase Premium
= 0 ? "text-emerald-600" : "text-red-500"}`}>{premium >= 0 ? "+" : ""}{pct(premium, 2)}
机构: {premium > 0.0005 ? "偏多" : premium < -0.0005 ? "偏空" : "中性"}
);
}
// ─── 实时指标卡片 ────────────────────────────────────────────────
function IndicatorCards({ symbol }: { symbol: Symbol }) {
const [data, setData] = useState(null);
useEffect(() => {
const fetch = async () => {
try {
const res = await authFetch("/api/signals/latest");
if (!res.ok) return;
const json = await res.json();
setData(json[symbol] || null);
} catch {}
};
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 (
{/* 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)}
超大单门槛
{/* 信号状态(V5.1) */}
当前信号
{data.signal === "LONG" ? "🟢 做多" : data.signal === "SHORT" ? "🔴 做空" : "⚪ 无信号"}
总分
{data.score}/100
档位: {data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : data.tier === "light" ? "轻仓" : "不开仓"}
{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.1 信号引擎
五层100分评分 + 市场拥挤度 + 环境确认
{(["BTC", "ETH"] as Symbol[]).map(s => (
))}
{/* 实时指标卡片 */}
{/* Market Indicators */}
Market Indicators
{/* CVD三轨图 */}
CVD三轨 + 币价
蓝色面积=CVD_fast(30m) · 紫色虚线=CVD_mid(4h) · 橙色虚线=币价
{WINDOWS.map(w => (
))}
{/* 说明 */}
V5.1评分逻辑:方向层45分 + 拥挤层20分 + 环境层15分 + 确认层15分 + 辅助层5分(方向加速可额外+5)。
开仓档位:<60不开仓,60-74轻仓,75-84标准仓位,≥85允许加仓;信号冷却10分钟。
);
}