"use client"; /** * K 线图(v3 §20 Chart):蜡烛图 + 成交量 + MA 指标 + 事件标记。 * * 标记语义(v3 §20.3 Signal↔Fill 严格区分): * - fills(实际成交):实心标记 —— fill_buy 红色 ▲ / fill_sell 绿色 ▼ * - signals(未成交意图):空心标记 —— signal_buy 红圈 ▲ / signal_sell 绿圈 * 组件只做展示,绝不自行计算(数据来自后端 Chart API)。 * * 基于 ECharts candlestick(ECharts 实现);另一套实现见 CandleChartLW.tsx(TradingView * Lightweight Charts),两套共用同一 props 接口(chartTypes.ts)供对比。 */ import { useEffect, useRef } from "react"; import * as echarts from "echarts/core"; import { BarChart, CandlestickChart, LineChart, ScatterChart } from "echarts/charts"; import { GridComponent, TooltipComponent } from "echarts/components"; import { CanvasRenderer } from "echarts/renderers"; import type { ChartMarker } from "@/lib/types"; import type { CandleChartProps } from "./chartTypes"; export type { CandleDatum } from "./chartTypes"; echarts.use([ CandlestickChart, LineChart, BarChart, ScatterChart, GridComponent, TooltipComponent, CanvasRenderer, ]); const C = { faint: "#8b98b2", line: "rgba(150,165,195,0.13)", tooltipBg: "#141d30", tooltipBorder: "rgba(150,165,195,0.28)", up: "#e5484d", down: "#2fb36b", ma20: "#3fb6ff", ma60: "#d4a72c", }; export function CandleChart({ data, volume = [], indicators = {}, fills = [], signals = [], height = 480, name = "", }: CandleChartProps) { const ref = useRef(null); useEffect(() => { if (!ref.current || data.length === 0) return; const chart = echarts.init(ref.current); const dates = data.map((d) => d.time); const vol = volume ?? []; // echarts option 结构较宽,series 用宽松类型(组件层实现细节) // eslint-disable-next-line @typescript-eslint/no-explicit-any const series: any[] = [ { name: "K线", type: "candlestick", data: data.map((d) => [d.open, d.close, d.low, d.high]), itemStyle: { color: C.up, // 阳线(收盘>开盘)红涨 color0: C.down, borderColor: C.up, borderColor0: C.down, }, }, ]; for (const [name, pts] of Object.entries(indicators)) { const vals = dates.map((t) => { const p = pts.find((x) => x.time === t); return p?.value ?? null; }); series.push({ name, type: "line", data: vals, smooth: false, showSymbol: false, lineStyle: { width: 1.2 }, itemStyle: { color: C[name as keyof typeof C] ?? "#b98bff" }, connectNulls: true, }); } // 成交标记(实心)与未成交信号(空心):以散点叠加在价格坐标 // eslint-disable-next-line @typescript-eslint/no-explicit-any const mk = (m: ChartMarker, filled: boolean): any => { const buy = m.kind.endsWith("buy"); return { value: [m.time, m.price ?? 0], symbol: filled ? (buy ? "triangle" : "triangle") : buy ? "triangle" : "triangle", symbolRotate: buy ? 0 : 180, symbolSize: 13, itemStyle: { color: filled ? (buy ? C.up : C.down) : "transparent", borderColor: buy ? C.up : C.down, borderWidth: 1.6, }, tooltip: { formatter: m.text.join("
") }, }; }; series.push( { name: "成交", type: "scatter", data: fills.map((m) => mk(m, true)), z: 6 }, { name: "信号(未成交)", type: "scatter", data: signals.map((m) => mk(m, false)), z: 5 } ); const hasVol = vol.length > 0; const mainGridBottom = hasVol ? 58 : 14; series.push({ name: "成交量", type: "bar", xAxisIndex: 1, yAxisIndex: 1, data: dates.map((t) => { const p = vol.find((x) => x.time === t); return p?.value ?? 0; }), itemStyle: { color: "rgba(150,165,195,0.35)" }, }); chart.setOption({ backgroundColor: "transparent", tooltip: { trigger: "axis", axisPointer: { type: "cross" }, backgroundColor: C.tooltipBg, borderColor: C.tooltipBorder, textStyle: { color: "#e9eef7", fontSize: 12 }, }, axisPointer: { link: [{ xAxisIndex: "all" }] }, grid: [ { left: 8, right: 18, top: 16, height: hasVol ? "62%" : "82%", containLabel: true }, ...(hasVol ? [{ left: 8, right: 18, top: hasVol ? "76%" : undefined, height: "14%", containLabel: true }] : []), ], xAxis: [ { type: "category", data: dates, boundaryGap: true, axisLine: { lineStyle: { color: C.line } }, axisTick: { show: false }, axisLabel: { color: C.faint, fontSize: 10 }, splitLine: { show: false }, }, ...(hasVol ? [{ type: "category", gridIndex: 1, data: dates, boundaryGap: true, axisLine: { lineStyle: { color: C.line } }, axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false }, }] : []), ], yAxis: [ { type: "value", scale: true, splitNumber: 4, axisLabel: { color: C.faint, fontSize: 10 }, splitLine: { lineStyle: { color: C.line } }, }, ...(hasVol ? [{ type: "value", gridIndex: 1, scale: true, axisLabel: { color: C.faint, fontSize: 9 }, splitLine: { show: false }, }] : []), ], series, legend: { show: false, }, }); const onResize = () => chart.resize(); window.addEventListener("resize", onResize); return () => { window.removeEventListener("resize", onResize); chart.dispose(); }; }, [data, volume, indicators, fills, signals, height]); if (data.length === 0) return
暂无 K 线数据
; return
; }