diff --git a/frontend/app/kline/page.tsx b/frontend/app/kline/page.tsx index d0ba37d..cdafdcd 100644 --- a/frontend/app/kline/page.tsx +++ b/frontend/app/kline/page.tsx @@ -1,46 +1,35 @@ "use client"; import { useEffect, useRef, useState, useCallback } from "react"; -import { createChart, ColorType, CandlestickSeries, LineSeries } from "lightweight-charts"; +import { createChart, ColorType, CandlestickSeries } 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" }, + { 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; + open: number; high: number; low: number; close: number; + price_open: number; price_high: number; price_low: number; price_close: number; } -function chartOptions(height: number) { +function baseChartOptions(height: number) { return { layout: { background: { type: ColorType.Solid, color: "#ffffff" }, - textColor: "#64748b", - fontSize: 11, - }, - grid: { - vertLines: { color: "#f1f5f9" }, - horzLines: { color: "#f1f5f9" }, + 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"); @@ -51,14 +40,10 @@ function chartOptions(height: number) { }, }, timeScale: { - borderColor: "#e2e8f0", - timeVisible: true, - secondsVisible: false, + 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}`; + return `${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}`; }, }, rightPriceScale: { borderColor: "#e2e8f0" }, @@ -66,145 +51,119 @@ function chartOptions(height: number) { }; } -export default function KlinePage() { - const rateRef = useRef(null); - const priceRef = useRef(null); - const [symbol, setSymbol] = useState<"BTC" | "ETH">("BTC"); - const [interval, setInterval] = useState("1h"); - const [count, setCount] = useState(0); - const [loading, setLoading] = useState(true); - const rateChartRef = useRef | null>(null); - const priceChartRef = useRef | null>(null); +function IntervalBar({ value, onChange }: { value: string; onChange: (v: string) => void }) { + return ( +
+ {INTERVALS.map(iv => ( + + ))} +
+ ); +} - const fetchAndRender = useCallback(async () => { +function KChart({ + symbol, interval, mode, color, +}: { + symbol: string; interval: string; mode: "rate" | "price"; + color: { up: string; down: string }; +}) { + const containerRef = useRef(null); + const chartRef = useRef | 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 (!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", + 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, }); - 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]); + 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(() => { - setLoading(true); - fetchAndRender(); - const iv = window.setInterval(fetchAndRender, 30_000); + render(); + const iv = window.setInterval(render, 30_000); return () => { window.clearInterval(iv); - rateChartRef.current?.remove(); - priceChartRef.current?.remove(); - rateChartRef.current = null; - priceChartRef.current = null; + chartRef.current?.remove(); + chartRef.current = null; }; - }, [fetchAndRender]); + }, [render]); + + return ( +
+

{count} 根K线

+
+
+ ); +} + +export default function KlinePage() { + const [symbol, setSymbol] = useState<"BTC" | "ETH">("BTC"); + const [rateInterval, setRateInterval] = useState("1h"); + const [priceInterval, setPriceInterval] = useState("1h"); return (
- {/* 标题+控制栏 */} -
-
-

费率 K 线

-

- 本地2秒粒度数据聚合 · 已记录 {count.toLocaleString()} 根K线 -

-
-
- {/* 币种 */} + {/* 标题+币种 */} +
+

费率 K 线

+
{(["BTC", "ETH"] as const).map(s => ( ))} - - {/* 周期 */} - {INTERVALS.map(iv => ( - - ))}
- {loading &&
加载中...
} - {/* 费率K线 */} -
-
-

{symbol} 资金费率 K 线(万分之)

- 原始值×10000 · 绿涨红跌 · 代表多头情绪强弱 +
+
+
+

{symbol} 资金费率(万分之)

+

原始值×10000 · 绿涨红跌 · 代表多头情绪

+
+
-
+
{/* 价格K线 */} -
-
-

{symbol} 标记价格 K 线(USD)

- 蓝涨紫跌 · 与费率对比观察趋势 +
+
+
+

{symbol} 标记价格(USD)

+

蓝涨紫跌 · 与费率对比观察趋势

+
+
-
+
说明: - 数据来自本地rate_snapshots表(每2秒一条),在前端聚合为各时间周期K线,每30秒自动刷新。数据仅从露露服务器启动后开始积累,历史越长越有价值。 + 数据来自本地rate_snapshots表(每2秒一条),后端自动采集,永久保留。每30秒刷新。
);