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

171 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, useRef, useState, useCallback } from "react";
import { createChart, ColorType, CandlestickSeries } from "lightweight-charts";
const INTERVALS = [
{ label: "1m", value: "1m" },
{ label: "5m", value: "5m" },
{ label: "30m", value: "30m" },
{ label: "1h", value: "1h" },
{ label: "4h", value: "4h" },
{ label: "8h", 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 baseChartOptions(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) => {
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);
return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`;
},
},
rightPriceScale: { borderColor: "#e2e8f0" },
height,
};
}
function IntervalBar({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return (
<div className="flex flex-wrap gap-1.5 text-xs">
{INTERVALS.map(iv => (
<button key={iv.value} onClick={() => onChange(iv.value)}
className={`px-2.5 py-1 rounded border transition-colors ${value === iv.value ? "bg-slate-800 text-white border-slate-800" : "border-slate-200 text-slate-500 hover:border-slate-400"}`}>
{iv.label}
</button>
))}
</div>
);
}
function KChart({
symbol, interval, mode, color,
}: {
symbol: string; interval: string; mode: "rate" | "price";
color: { up: string; down: string };
}) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<ReturnType<typeof createChart> | null>(null);
const [count, setCount] = useState(0);
const render = 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 (!containerRef.current) return;
chartRef.current?.remove();
const chart = createChart(containerRef.current, baseChartOptions(260));
chartRef.current = chart;
const series = chart.addSeries(CandlestickSeries, {
upColor: color.up, downColor: color.down,
borderUpColor: color.up, borderDownColor: color.down,
wickUpColor: color.up, wickDownColor: color.down,
});
if (mode === "rate") {
series.setData(bars.map(b => ({ time: b.time as any, open: b.open, high: b.high, low: b.low, close: b.close })));
} else {
series.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 })));
}
chart.timeScale().fitContent();
containerRef.current.querySelectorAll("a").forEach(a => (a as HTMLElement).style.display = "none");
} catch (e) { console.error(e); }
}, [symbol, interval, mode, color.up, color.down]);
useEffect(() => {
render();
const iv = window.setInterval(render, 30_000);
return () => {
window.clearInterval(iv);
chartRef.current?.remove();
chartRef.current = null;
};
}, [render]);
return (
<div>
<p className="text-xs text-slate-400 mb-1">{count} K线</p>
<div ref={containerRef} className="w-full" style={{ height: 260 }} />
</div>
);
}
export default function KlinePage() {
const [symbol, setSymbol] = useState<"BTC" | "ETH">("BTC");
const [rateInterval, setRateInterval] = useState("1h");
const [priceInterval, setPriceInterval] = useState("1h");
return (
<div className="space-y-5">
{/* 标题+币种 */}
<div className="flex items-center justify-between flex-wrap gap-3">
<h1 className="text-2xl font-bold text-slate-900"> K 线</h1>
<div className="flex gap-2 text-sm">
{(["BTC", "ETH"] as const).map(s => (
<button key={s} onClick={() => setSymbol(s)}
className={`px-4 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>
))}
</div>
</div>
{/* 费率K线 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4 space-y-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-slate-700 font-semibold text-sm">{symbol} </h2>
<p className="text-xs text-slate-400">×10000 · 绿 · </p>
</div>
<IntervalBar value={rateInterval} onChange={setRateInterval} />
</div>
<KChart symbol={symbol} interval={rateInterval} mode="rate" color={{ up: "#16a34a", down: "#dc2626" }} />
</div>
{/* 价格K线 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4 space-y-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-slate-700 font-semibold text-sm">{symbol} USD</h2>
<p className="text-xs text-slate-400"> · </p>
</div>
<IntervalBar value={priceInterval} onChange={setPriceInterval} />
</div>
<KChart symbol={symbol} interval={priceInterval} mode="price" color={{ up: "#2563eb", down: "#7c3aed" }} />
</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表230
</div>
</div>
);
}