139 lines
6.6 KiB
TypeScript
139 lines
6.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import {
|
||
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
||
ResponsiveContainer, ReferenceLine, CartesianGrid
|
||
} from "recharts";
|
||
|
||
interface Snapshot {
|
||
ts: number;
|
||
btc_rate: number;
|
||
eth_rate: number;
|
||
btc_price: number;
|
||
eth_price: number;
|
||
}
|
||
|
||
interface ChartPoint {
|
||
time: string;
|
||
btcRate: number;
|
||
ethRate: number;
|
||
btcPrice: number;
|
||
ethPrice: number;
|
||
}
|
||
|
||
export default function LivePage() {
|
||
const [data, setData] = useState<ChartPoint[]>([]);
|
||
const [count, setCount] = useState(0);
|
||
const [loading, setLoading] = useState(true);
|
||
const [hours, setHours] = useState(2);
|
||
|
||
const fetchSnapshots = useCallback(async () => {
|
||
try {
|
||
const r = await fetch(`/api/snapshots?hours=${hours}&limit=3600`);
|
||
const json = await r.json();
|
||
const rows: Snapshot[] = json.data || [];
|
||
setCount(json.count || 0);
|
||
// 降采样:每30条取1条,避免图表过密
|
||
const step = Math.max(1, Math.floor(rows.length / 300));
|
||
const sampled = rows.filter((_, i) => i % step === 0);
|
||
setData(sampled.map(row => ({
|
||
time: new Date(row.ts * 1000).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
||
btcRate: parseFloat((row.btc_rate * 100).toFixed(5)),
|
||
ethRate: parseFloat((row.eth_rate * 100).toFixed(5)),
|
||
btcPrice: row.btc_price,
|
||
ethPrice: row.eth_price,
|
||
})));
|
||
} catch { /* ignore */ } finally {
|
||
setLoading(false);
|
||
}
|
||
}, [hours]);
|
||
|
||
useEffect(() => {
|
||
fetchSnapshots();
|
||
const iv = setInterval(fetchSnapshots, 10_000);
|
||
return () => clearInterval(iv);
|
||
}, [fetchSnapshots]);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900">实时费率变动</h1>
|
||
<p className="text-slate-500 text-sm mt-1">
|
||
8小时结算周期内的实时费率曲线 · 已记录 <span className="text-blue-600 font-medium">{count.toLocaleString()}</span> 条快照
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2 text-sm">
|
||
{[1, 2, 6, 12, 24].map(h => (
|
||
<button
|
||
key={h}
|
||
onClick={() => setHours(h)}
|
||
className={`px-3 py-1.5 rounded-lg border transition-colors ${hours === h ? "bg-blue-600 text-white border-blue-600" : "border-slate-200 text-slate-600 hover:border-blue-400"}`}
|
||
>
|
||
{h}h
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="text-slate-400 py-12 text-center">加载中...</div>
|
||
) : data.length === 0 ? (
|
||
<div className="rounded-xl border border-slate-200 bg-slate-50 p-12 text-center text-slate-400">
|
||
暂无数据(后端刚启动,2秒后开始积累)
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* 费率图 */}
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||
<h2 className="text-slate-700 font-semibold mb-1">资金费率实时变动</h2>
|
||
<p className="text-slate-400 text-xs mb-4">正值=多头付空头,负值=空头付多头。费率上升=多头情绪加热</p>
|
||
<ResponsiveContainer width="100%" height={220}>
|
||
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
||
<YAxis tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} axisLine={false}
|
||
tickFormatter={v => `${v.toFixed(3)}%`} width={60} />
|
||
<Tooltip formatter={(v: number) => [`${v.toFixed(5)}%`]}
|
||
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} />
|
||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="4 2" />
|
||
<Line type="monotone" dataKey="btcRate" name="BTC费率" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
|
||
<Line type="monotone" dataKey="ethRate" name="ETH费率" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
|
||
{/* 价格图 */}
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||
<h2 className="text-slate-700 font-semibold mb-1">标记价格走势</h2>
|
||
<p className="text-slate-400 text-xs mb-4">与费率对比观察:价格快速拉升时,资金费率通常同步上涨(多头加杠杆)</p>
|
||
<ResponsiveContainer width="100%" height={220}>
|
||
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||
<XAxis dataKey="time" tick={{ fill: "#94a3b8", fontSize: 10 }} tickLine={false} interval="preserveStartEnd" />
|
||
<YAxis yAxisId="btc" orientation="left" tick={{ fill: "#2563eb", fontSize: 10 }} tickLine={false} axisLine={false}
|
||
tickFormatter={v => `$${(v/1000).toFixed(0)}k`} width={55} />
|
||
<YAxis yAxisId="eth" orientation="right" tick={{ fill: "#7c3aed", fontSize: 10 }} tickLine={false} axisLine={false}
|
||
tickFormatter={v => `$${v.toFixed(0)}`} width={55} />
|
||
<Tooltip formatter={(v: number, name: string) => [name.includes("BTC") ? `$${v.toLocaleString()}` : `$${v.toFixed(2)}`, name]}
|
||
contentStyle={{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, fontSize: 12 }} />
|
||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||
<Line yAxisId="btc" type="monotone" dataKey="btcPrice" name="BTC价格" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
|
||
<Line yAxisId="eth" type="monotone" dataKey="ethPrice" name="ETH价格" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
|
||
{/* 说明 */}
|
||
<div className="rounded-lg border border-blue-100 bg-blue-50 px-5 py-3 text-sm text-slate-600">
|
||
<span className="text-blue-600 font-medium">数据说明:</span>
|
||
每2秒从Binance拉取实时溢价指数,本地永久存储。这是8小时结算周期内费率变动的原始数据,不在任何公开数据源中提供。
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|