Files
qlib/frontend/web/components/StockChart/CandleChartLW.tsx
T
Simon 03fb463216 feat(web): K线图库对比 —— TradingView Lightweight Charts 实现 + 双库切换
- 按确认方案「两套实现并存对比」:
  - npm 依赖 lightweight-charts@4.2.3(v3 §21 推荐库)
  - components/StockChart/chartTypes.ts:共享 props 接口
  - CandleChart.tsx(ECharts 版,重构复用共享类型)+ CandleChartLW.tsx
    (Lightweight 版:蜡烛+成交量副刻度+MA+成交实心箭头/未成交信号圆点 markers)
  - StockChart.tsx wrapper;个股页顶部「ECharts / Lightweight」切换(默认 Lightweight)
- /stocks/600519.SH 冒烟 200;tsc --noEmit 通过
2026-09-09 07:21:07 +08:00

155 lines
4.4 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";
/**
* K 线图 —— TradingView Lightweight Charts 实现(v3 §21 推荐库)。
*
* 与 ECharts 版(CandleChart.tsx)共用同一 props 接口(chartTypes.ts),
* 供「两套实现并存对比」,组件层可整体替换。
* 标记语义(v3 §20.3):成交 fills = 实心箭头;未成交信号 = 空心圆点。
* 组件只展示,不重算。
*/
import { useEffect, useRef } from "react";
import {
createChart,
ColorType,
CrosshairMode,
type IChartApi,
type ISeriesApi,
type SeriesMarker,
type Time,
} from "lightweight-charts";
import type { CandleChartProps } from "./chartTypes";
const C = {
faint: "#8b98b2",
grid: "rgba(150,165,195,0.09)",
up: "#e5484d",
down: "#2fb36b",
ma20: "#3fb6ff",
ma60: "#d4a72c",
text: "#e9eef7",
markerBuy: "#3ddc97",
markerSell: "#ff5a5a",
markerSig: "#b98bff",
};
function toTime(t: string): Time {
return t as Time; // 'YYYY-MM-DD'(v4 支持字符串 business day)
}
export function CandleChartLW({
data,
volume = [],
indicators = {},
fills = [],
signals = [],
height = 480,
name = "",
}: CandleChartProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current || data.length === 0) return;
const chart: IChartApi = createChart(ref.current, {
autoSize: true,
height,
layout: {
background: { type: ColorType.Solid, color: "transparent" },
textColor: C.text,
fontSize: 11,
},
grid: {
vertLines: { color: C.grid },
horzLines: { color: C.grid },
},
rightPriceScale: { borderColor: C.grid },
timeScale: { borderColor: C.grid, timeVisible: false },
crosshair: { mode: CrosshairMode.Normal },
});
const candle: ISeriesApi<"Candlestick"> = chart.addCandlestickSeries({
upColor: C.up,
downColor: C.down,
borderUpColor: C.up,
borderDownColor: C.down,
wickUpColor: C.up,
wickDownColor: C.down,
});
candle.setData(
data.map((d) => ({
time: toTime(d.time),
open: d.open,
high: d.high,
low: d.low,
close: d.close,
}))
);
// 成交量(独立价格刻度,置于底部 20%)
const vol: ISeriesApi<"Histogram"> = chart.addHistogramSeries({
priceFormat: { type: "volume" },
priceScaleId: "vol",
lastValueVisible: false,
priceLineVisible: false,
});
chart.priceScale("vol").applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
vol.setData(
volume.map((v) => ({ time: toTime(v.time), value: v.value ?? 0, color: C.grid }))
);
// MA 指标线
for (const [name, pts] of Object.entries(indicators)) {
const line = chart.addLineSeries({
color: name === "ma60" ? C.ma60 : C.ma20,
lineWidth: 1,
priceLineVisible: false,
lastValueVisible: false,
crosshairMarkerVisible: false,
});
line.setData(
pts
.filter((p) => p.value != null)
.map((p) => ({ time: toTime(p.time), value: p.value as number }))
);
}
// 事件标记:成交=实心箭头;未成交信号=圆点
const markers: SeriesMarker<Time>[] = [];
for (const m of fills) {
const buy = m.kind.endsWith("buy");
markers.push({
time: toTime(m.time),
position: buy ? "belowBar" : "aboveBar",
color: buy ? C.markerBuy : C.markerSell,
shape: buy ? "arrowUp" : "arrowDown",
size: 1,
text: buy ? "B" : "S",
});
}
for (const m of signals) {
const buy = m.kind.endsWith("buy");
markers.push({
time: toTime(m.time),
position: buy ? "belowBar" : "aboveBar",
color: C.markerSig,
shape: "circle",
size: 1,
text: buy ? "sigB" : "sigS",
});
}
if (markers.length) candle.setMarkers(markers);
if (data.length) chart.timeScale().fitContent();
const onResize = () => chart.applyOptions({ width: ref.current?.clientWidth });
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
chart.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, volume, indicators, fills, signals, height]);
if (data.length === 0) return <div className="hint">暂无 K 线数据</div>;
return <div ref={ref} style={{ height, width: "100%" }} aria-label={`${name} K 线图(Lightweight)`} />;
}