"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { createChart, ColorType, CandlestickSeries } from "lightweight-charts"; import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint, SignalHistoryItem, KBar } from "@/lib/api"; import RateCard from "@/components/RateCard"; import StatsCard from "@/components/StatsCard"; import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, ReferenceLine } from "recharts"; // ─── K线子组件 ────────────────────────────────────────────────── const INTERVALS = [ { label: "1m", value: "1m" }, { label: "5m", value: "5m" }, { label: "30m", value: "30m" }, { label: "1h", value: "1h" }, { label: "4h", value: "4h" }, { label: "8h", value: "8h" }, { label: "日", value: "1d" }, { label: "周", value: "1w" }, { label: "月", value: "1M" }, ]; function bjtTimeFormatter(ts: number) { const d = new Date((ts + 8 * 3600) * 1000); return `${d.getUTCFullYear()}-${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 baseChartOpts(height: number) { return { layout: { background: { type: ColorType.Solid, color: "#ffffff" }, textColor: "#64748b", fontSize: 11 }, grid: { vertLines: { color: "#f1f5f9" }, horzLines: { color: "#f1f5f9" } }, localization: { timeFormatter: bjtTimeFormatter }, timeScale: { borderColor: "#e2e8f0", timeVisible: true, secondsVisible: false, tickMarkFormatter: (ts: number) => { const d = new Date((ts + 8 * 3600) * 1000); return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`; }, }, rightPriceScale: { borderColor: "#e2e8f0" }, height, }; } function MiniKChart({ symbol, interval, mode, colors }: { symbol: string; interval: string; mode: "rate"|"price"; colors: { up: string; down: string }; }) { const ref = useRef(null); const chartRef = useRef | null>(null); const render = useCallback(async () => { try { const json = await api.kline(symbol, interval); const bars: KBar[] = json.data || []; if (!ref.current) return; chartRef.current?.remove(); const chart = createChart(ref.current, baseChartOpts(220)); chartRef.current = chart; const series = chart.addSeries(CandlestickSeries, { upColor: colors.up, downColor: colors.down, borderUpColor: colors.up, borderDownColor: colors.down, wickUpColor: colors.up, wickDownColor: colors.down, }); series.setData(mode === "rate" ? bars.map(b => ({ time: b.time as any, open: b.open, high: b.high, low: b.low, close: b.close })) : bars.map(b => ({ time: b.time as any, open: b.price_open, high: b.price_high, low: b.price_low, close: b.price_close })) ); chart.timeScale().fitContent(); ref.current.querySelectorAll("a").forEach(a => (a as HTMLElement).style.display = "none"); } catch {} }, [symbol, interval, mode, colors.up, colors.down]); useEffect(() => { render(); const iv = window.setInterval(render, 30_000); return () => { window.clearInterval(iv); chartRef.current?.remove(); chartRef.current = null; }; }, [render]); return
; } function IntervalPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) { return (
{INTERVALS.map(iv => ( ))}
); } // ─── 主仪表盘 ──────────────────────────────────────────────────── export default function Dashboard() { const [rates, setRates] = useState(null); const [stats, setStats] = useState(null); const [history, setHistory] = useState(null); const [signals, setSignals] = useState([]); const [status, setStatus] = useState<"loading"|"running"|"error">("loading"); const [lastUpdate, setLastUpdate] = useState(""); const [symbol, setSymbol] = useState<"BTC"|"ETH">("BTC"); const [rateInterval, setRateInterval] = useState("1h"); const [priceInterval, setPriceInterval] = useState("1h"); const fetchRates = useCallback(async () => { try { setRates(await api.rates()); setStatus("running"); setLastUpdate(new Date().toLocaleTimeString("zh-CN")); } catch { setStatus("error"); } }, []); const fetchSlow = useCallback(async () => { try { const [s, h, sig] = await Promise.all([api.stats(), api.history(), api.signalsHistory()]); setStats(s); setHistory(h); setSignals(sig.items || []); } catch {} }, []); useEffect(() => { fetchRates(); fetchSlow(); const r = setInterval(fetchRates, 2_000); const s = setInterval(fetchSlow, 120_000); return () => { clearInterval(r); clearInterval(s); }; }, [fetchRates, fetchSlow]); // 历史图数据 const btcMap = new Map((history?.BTC ?? []).map((p: HistoryPoint) => [p.timestamp.slice(0,13), p.fundingRate * 100])); const ethMap = new Map((history?.ETH ?? []).map((p: HistoryPoint) => [p.timestamp.slice(0,13), p.fundingRate * 100])); const allTimes = Array.from(new Set([...btcMap.keys(), ...ethMap.keys()])).sort(); const historyChart = allTimes.slice(-42).map(t => ({ time: t.slice(5).replace("T"," "), BTC: btcMap.get(t) ?? null, ETH: ethMap.get(t) ?? null })); const tableRows = allTimes.slice().reverse().slice(0,30).map(t => ({ time: t.replace("T"," "), btc: btcMap.get(t), eth: ethMap.get(t) })); return (
{/* 标题 */}

资金费率套利监控

实时监控 BTC / ETH 永续合约资金费率

{status==="running"?"运行中":status==="error"?"连接失败":"加载中..."} {lastUpdate && 更新于 {lastUpdate}}
{/* 费率卡片 */}
{/* 统计卡片 */} {stats && (
)} {/* K线 */}

K 线图

{(["BTC","ETH"] as const).map(s => ( ))}
{/* 费率K */}
{symbol} 资金费率(万分之)
{/* 价格K */}
{symbol} 标记价格(USD)
{/* 历史费率走势 */} {historyChart.length > 0 && (

历史费率走势(过去7天)

`${v.toFixed(3)}%`} width={58} /> [`${Number(v).toFixed(4)}%`]} contentStyle={{ background:"#fff", border:"1px solid #e2e8f0", borderRadius:8, fontSize:12 }} />
)} {/* 历史明细 + 信号历史 并排 */}
{/* 历史明细 */} {tableRows.length > 0 && (

历史费率明细(最近30条)

{tableRows.map((row, i) => ( ))}
时间 BTC ETH
{row.time} =0?"text-emerald-600":"text-red-500"}`}> {row.btc!=null?`${row.btc.toFixed(4)}%`:"--"} =0?"text-emerald-600":"text-red-500"}`}> {row.eth!=null?`${row.eth.toFixed(4)}%`:"--"}
)} {/* 信号历史 */}

信号历史

{signals.length === 0 ? ( ) : signals.map(row => ( ))}
时间 币种 年化
暂无信号(年化>10%时自动触发)
{new Date(row.sent_at).toLocaleString("zh-CN")} {row.symbol} {row.annualized}%
{/* 策略说明 */}
策略原理: 持有现货多头 + 永续空头,每8小时收取资金费率,赚取无方向风险的稳定收益。
); }