arbitrage-engine/frontend/app/live/page.tsx

139 lines
6.6 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 {
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, name) => [name?.toString().includes("BTC") ? `$${Number(v).toLocaleString()}` : `$${Number(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>
2Binance拉取实时溢价指数8
</div>
</>
)}
</div>
);
}