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

212 lines
7.5 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, useRef, useState, useCallback } from "react";
import { createChart, ColorType, CandlestickSeries, LineSeries } from "lightweight-charts";
const INTERVALS = [
{ label: "1分钟", value: "1m" },
{ label: "5分钟", value: "5m" },
{ label: "30分钟", value: "30m" },
{ label: "1小时", value: "1h" },
{ label: "4小时", value: "4h" },
{ label: "8小时", value: "8h" },
{ label: "日线", value: "1d" },
{ label: "周线", value: "1w" },
{ label: "月线", value: "1M" },
];
interface KBar {
time: number;
open: number;
high: number;
low: number;
close: number;
price_open: number;
price_high: number;
price_low: number;
price_close: number;
}
function chartOptions(height: number) {
return {
layout: {
background: { type: ColorType.Solid, color: "#ffffff" },
textColor: "#64748b",
fontSize: 11,
},
grid: {
vertLines: { color: "#f1f5f9" },
horzLines: { color: "#f1f5f9" },
},
localization: {
timeFormatter: (ts: number) => {
// UTC+8 北京时间
const d = new Date((ts + 8 * 3600) * 1000);
const Y = d.getUTCFullYear();
const M = String(d.getUTCMonth() + 1).padStart(2, "0");
const D = String(d.getUTCDate()).padStart(2, "0");
const h = String(d.getUTCHours()).padStart(2, "0");
const m = String(d.getUTCMinutes()).padStart(2, "0");
return `${Y}-${M}-${D} ${h}:${m}`;
},
},
timeScale: {
borderColor: "#e2e8f0",
timeVisible: true,
secondsVisible: false,
tickMarkFormatter: (ts: number) => {
const d = new Date((ts + 8 * 3600) * 1000);
const h = String(d.getUTCHours()).padStart(2, "0");
const m = String(d.getUTCMinutes()).padStart(2, "0");
return `${h}:${m}`;
},
},
rightPriceScale: { borderColor: "#e2e8f0" },
height,
};
}
export default function KlinePage() {
const rateRef = useRef<HTMLDivElement>(null);
const priceRef = useRef<HTMLDivElement>(null);
const [symbol, setSymbol] = useState<"BTC" | "ETH">("BTC");
const [interval, setInterval] = useState("5m");
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(true);
const rateChartRef = useRef<ReturnType<typeof createChart> | null>(null);
const priceChartRef = useRef<ReturnType<typeof createChart> | null>(null);
const fetchAndRender = useCallback(async () => {
try {
const r = await fetch(`/api/kline?symbol=${symbol}&interval=${interval}&limit=500`);
const json = await r.json();
const bars: KBar[] = json.data || [];
setCount(json.count || 0);
if (!rateRef.current || !priceRef.current) return;
// 销毁旧图表
rateChartRef.current?.remove();
priceChartRef.current?.remove();
// 费率K线
const rateChart = createChart(rateRef.current, chartOptions(260));
rateChartRef.current = rateChart;
const rateSeries = rateChart.addSeries(CandlestickSeries, {
upColor: "#16a34a",
downColor: "#dc2626",
borderUpColor: "#16a34a",
borderDownColor: "#dc2626",
wickUpColor: "#16a34a",
wickDownColor: "#dc2626",
});
rateSeries.setData(bars.map(b => ({
time: b.time as any,
open: b.open,
high: b.high,
low: b.low,
close: b.close,
})));
rateChart.timeScale().fitContent();
// 隐藏 TradingView 水印
rateRef.current.querySelectorAll("a").forEach(a => a.style.display = "none");
// 价格K线
const priceChart = createChart(priceRef.current, chartOptions(260));
priceChartRef.current = priceChart;
const priceSeries = priceChart.addSeries(CandlestickSeries, {
upColor: "#2563eb",
downColor: "#7c3aed",
borderUpColor: "#2563eb",
borderDownColor: "#7c3aed",
wickUpColor: "#2563eb",
wickDownColor: "#7c3aed",
});
priceSeries.setData(bars.map(b => ({
time: b.time as any,
open: b.price_open,
high: b.price_high,
low: b.price_low,
close: b.price_close,
})));
priceChart.timeScale().fitContent();
// 隐藏 TradingView 水印
priceRef.current.querySelectorAll("a").forEach(a => a.style.display = "none");
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, [symbol, interval]);
useEffect(() => {
setLoading(true);
fetchAndRender();
const iv = window.setInterval(fetchAndRender, 30_000);
return () => {
window.clearInterval(iv);
rateChartRef.current?.remove();
priceChartRef.current?.remove();
rateChartRef.current = null;
priceChartRef.current = null;
};
}, [fetchAndRender]);
return (
<div className="space-y-5">
{/* 标题+控制栏 */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-slate-900"> K 线</h1>
<p className="text-slate-500 text-sm mt-1">
2 · <span className="text-blue-600 font-medium">{count.toLocaleString()}</span> K线
</p>
</div>
<div className="flex flex-wrap gap-2 text-sm">
{/* 币种 */}
{(["BTC", "ETH"] as const).map(s => (
<button key={s} onClick={() => setSymbol(s)}
className={`px-3 py-1.5 rounded-lg border font-medium transition-colors ${symbol === s ? "bg-blue-600 text-white border-blue-600" : "border-slate-200 text-slate-600 hover:border-blue-400"}`}>
{s}
</button>
))}
<span className="w-px bg-slate-200 mx-1" />
{/* 周期 */}
{INTERVALS.map(iv => (
<button key={iv.value} onClick={() => setInterval(iv.value)}
className={`px-3 py-1.5 rounded-lg border transition-colors ${interval === iv.value ? "bg-slate-800 text-white border-slate-800" : "border-slate-200 text-slate-600 hover:border-slate-400"}`}>
{iv.label}
</button>
))}
</div>
</div>
{loading && <div className="text-slate-400 py-8 text-center">...</div>}
{/* 费率K线 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-2">
<h2 className="text-slate-700 font-semibold text-sm">{symbol} K 线%</h2>
<span className="text-xs text-slate-400">绿 · </span>
</div>
<div ref={rateRef} className="w-full" style={{ height: 260 }} />
</div>
{/* 价格K线 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-2">
<h2 className="text-slate-700 font-semibold text-sm">{symbol} K 线USD</h2>
<span className="text-xs text-slate-400"> · </span>
</div>
<div ref={priceRef} className="w-full" style={{ height: 260 }} />
</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>
rate_snapshots表2K线30
</div>
</div>
);
}