arbitrage-engine/frontend/app/page.tsx

154 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 {
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>
)}
{/* 历史费率折线图 */}
{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">78</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>
);
}