163 lines
7.4 KiB
TypeScript
163 lines
7.4 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback } from "react";
|
||
import { api, RatesResponse, StatsResponse, HistoryResponse, HistoryPoint } from "@/lib/api";
|
||
import RateCard from "@/components/RateCard";
|
||
import StatsCard from "@/components/StatsCard";
|
||
import FundingChart from "@/components/FundingChart";
|
||
import {
|
||
LineChart, Line, XAxis, YAxis, Tooltip, Legend,
|
||
ResponsiveContainer, ReferenceLine
|
||
} from "recharts";
|
||
|
||
export default function Dashboard() {
|
||
const [rates, setRates] = useState<RatesResponse | null>(null);
|
||
const [stats, setStats] = useState<StatsResponse | null>(null);
|
||
const [history, setHistory] = useState<HistoryResponse | null>(null);
|
||
const [status, setStatus] = useState<"loading" | "running" | "error">("loading");
|
||
const [lastUpdate, setLastUpdate] = useState<string>("");
|
||
|
||
const fetchRates = useCallback(async () => {
|
||
try {
|
||
const r = await api.rates();
|
||
setRates(r);
|
||
setStatus("running");
|
||
setLastUpdate(new Date().toLocaleTimeString("zh-CN"));
|
||
} catch {
|
||
setStatus("error");
|
||
}
|
||
}, []);
|
||
|
||
const fetchAll = useCallback(async () => {
|
||
try {
|
||
const [s, h] = await Promise.all([api.stats(), api.history()]);
|
||
setStats(s);
|
||
setHistory(h);
|
||
} catch {}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchRates();
|
||
fetchAll();
|
||
const rateInterval = setInterval(fetchRates, 2_000);
|
||
const slowInterval = setInterval(fetchAll, 120_000);
|
||
return () => { clearInterval(rateInterval); clearInterval(slowInterval); };
|
||
}, [fetchRates, fetchAll]);
|
||
|
||
// 历史费率表格数据
|
||
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 historyChartData = allTimes.slice(-42).map((t) => ({
|
||
time: t.slice(5).replace("T", " "),
|
||
BTC: btcMap.get(t) ?? null,
|
||
ETH: ethMap.get(t) ?? null,
|
||
}));
|
||
const tableData = allTimes.slice().reverse().slice(0, 50).map((t) => ({
|
||
time: t.replace("T", " "),
|
||
btc: btcMap.get(t),
|
||
eth: ethMap.get(t),
|
||
}));
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900">资金费率套利监控</h1>
|
||
<p className="text-slate-500 text-sm mt-1">实时监控 BTC / ETH 永续合约资金费率</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<span className={`w-2 h-2 rounded-full ${
|
||
status === "running" ? "bg-emerald-500 animate-pulse"
|
||
: status === "error" ? "bg-red-500" : "bg-slate-300"
|
||
}`} />
|
||
<span className={status === "running" ? "text-emerald-600" : status === "error" ? "text-red-600" : "text-slate-400"}>
|
||
{status === "running" ? "运行中" : status === "error" ? "连接失败" : "加载中..."}
|
||
</span>
|
||
{lastUpdate && <span className="text-slate-400 ml-2">更新于 {lastUpdate}</span>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Rate Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<RateCard asset="BTC" data={rates?.BTC ?? null} />
|
||
<RateCard asset="ETH" data={rates?.ETH ?? null} />
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
{stats && (
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||
<StatsCard title="BTC 套利" mean7d={stats.BTC.mean7d} annualized={stats.BTC.annualized} accent="blue" />
|
||
<StatsCard title="ETH 套利" mean7d={stats.ETH.mean7d} annualized={stats.ETH.annualized} accent="indigo" />
|
||
<StatsCard title="50/50 组合" mean7d={stats.combo.mean7d} annualized={stats.combo.annualized} accent="green" />
|
||
</div>
|
||
)}
|
||
|
||
{/* 7天走势图(FundingChart) */}
|
||
{history && (
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||
<h2 className="text-slate-800 font-semibold mb-4">过去7天资金费率走势</h2>
|
||
<FundingChart history={history} />
|
||
</div>
|
||
)}
|
||
|
||
{/* 历史费率折线图 */}
|
||
{allTimes.length > 0 && (
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-6">
|
||
<h2 className="text-slate-800 font-semibold mb-4">历史费率走势(过去7天,每8小时结算)</h2>
|
||
<ResponsiveContainer width="100%" height={220}>
|
||
<LineChart data={historyChartData} margin={{ top: 4, right: 8, bottom: 4, left: 8 }}>
|
||
<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(4)}%`]} 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="BTC" stroke="#2563eb" strokeWidth={1.5} dot={false} connectNulls />
|
||
<Line type="monotone" dataKey="ETH" stroke="#7c3aed" strokeWidth={1.5} dot={false} connectNulls />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
)}
|
||
|
||
{/* 历史费率明细表 */}
|
||
{tableData.length > 0 && (
|
||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-slate-100">
|
||
<h2 className="text-slate-800 font-semibold">历史费率明细(最近50条)</h2>
|
||
</div>
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-slate-200 bg-slate-50">
|
||
<th className="text-left px-4 py-3 text-slate-500 font-medium">时间</th>
|
||
<th className="text-right px-4 py-3 text-blue-600 font-medium">BTC 费率</th>
|
||
<th className="text-right px-4 py-3 text-violet-600 font-medium">ETH 费率</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{tableData.map((row, i) => (
|
||
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50">
|
||
<td className="px-4 py-2 text-slate-500 font-mono text-xs">{row.time}</td>
|
||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.btc == null ? "text-slate-400" : row.btc >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||
{row.btc != null ? `${row.btc.toFixed(4)}%` : "--"}
|
||
</td>
|
||
<td className={`px-4 py-2 text-right font-mono text-xs ${row.eth == null ? "text-slate-400" : row.eth >= 0 ? "text-emerald-600" : "text-red-500"}`}>
|
||
{row.eth != null ? `${row.eth.toFixed(4)}%` : "--"}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* Strategy note */}
|
||
<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>
|
||
持有现货多头 + 永续空头,每8小时收取资金费率,赚取无方向风险的稳定收益。
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|