feat: K线成交量独立pane、浅色调、移除KDJ
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* K 线图组件(基于 TradingView Lightweight Charts v5)
|
||||
* 主图:蜡烛/折线 + MA(5/10/20/30);
|
||||
* 副图:成交量(pane1)、MACD(pane2)、RSI(pane3)、KDJ(pane4),可开关。
|
||||
* 副图:成交量(pane1)、MACD(pane2)、RSI(pane3),可开关。
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type ISeriesApi as ISeriesAny,
|
||||
type Time,
|
||||
} from "lightweight-charts";
|
||||
import { sma, macd as calcMacd, rsi as calcRsi, kdj as calcKdj, type KBar } from "@/lib/indicators";
|
||||
import { sma, macd as calcMacd, rsi as calcRsi, type KBar } from "@/lib/indicators";
|
||||
|
||||
export interface KLineItem {
|
||||
time: string; // ISO 日期 yyyy-mm-dd(lightweight-charts 必需)
|
||||
@@ -34,7 +34,6 @@ export interface IndicatorToggles {
|
||||
ma: boolean;
|
||||
macd: boolean;
|
||||
rsi: boolean;
|
||||
kdj: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -47,6 +46,9 @@ interface Props {
|
||||
// lightweight-charts 无法解析 CSS 变量或 oklch() 颜色,直接用具体 hex 色。
|
||||
const UP = "#ef4444";
|
||||
const DOWN = "#22c55e";
|
||||
// 成交量柱:浅色调,避免与 K 线柱体视觉争夺
|
||||
const VOL_UP = "#fca5a5";
|
||||
const VOL_DOWN = "#86efac";
|
||||
const PRIMARY = "#ef4444";
|
||||
const TEXT = "#71717a";
|
||||
const GRID = "#e4e4e7";
|
||||
@@ -58,9 +60,6 @@ const MA_PERIODS = [5, 10, 20];
|
||||
const MACD_DIF = "#3b82f6";
|
||||
const MACD_DEA = "#f59e0b";
|
||||
const RSI_COLOR = "#a855f7";
|
||||
const KDJ_K = "#3b82f6";
|
||||
const KDJ_D = "#f59e0b";
|
||||
const KDJ_J = "#a855f7";
|
||||
|
||||
export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -71,10 +70,9 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
const maSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
||||
const macdSeriesRef = useRef<ISeriesAny<"Line" | "Histogram">[]>([]);
|
||||
const rsiSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
||||
const kdjSeriesRef = useRef<ISeriesAny<"Line">[]>([]);
|
||||
|
||||
// 容器高度随指标 pane 数量增长,避免主图被压缩
|
||||
const extraPanes = [indicators.macd, indicators.rsi, indicators.kdj].filter(Boolean).length;
|
||||
const extraPanes = [indicators.macd, indicators.rsi].filter(Boolean).length;
|
||||
// 固定图表高度:主图+成交量 300,每个指标副图 +95
|
||||
const chartHeight = 300 + extraPanes * 95;
|
||||
|
||||
@@ -101,11 +99,15 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
});
|
||||
chartRef.current = chart;
|
||||
|
||||
const vol = chart.addSeries(HistogramSeries, {
|
||||
priceFormat: { type: "volume" },
|
||||
priceScaleId: "vol",
|
||||
});
|
||||
vol.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
|
||||
// 成交量放到独立 pane1,避免与主图K线重叠
|
||||
const vol = chart.addSeries(
|
||||
HistogramSeries,
|
||||
{
|
||||
priceFormat: { type: "volume" },
|
||||
priceScaleId: "vol",
|
||||
},
|
||||
1,
|
||||
);
|
||||
volSeriesRef.current = vol;
|
||||
|
||||
return () => {
|
||||
@@ -116,7 +118,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
maSeriesRef.current = [];
|
||||
macdSeriesRef.current = [];
|
||||
rsiSeriesRef.current = [];
|
||||
kdjSeriesRef.current = [];
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -222,25 +223,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
);
|
||||
rsiSeriesRef.current.push(s);
|
||||
}
|
||||
|
||||
// --- KDJ(pane4)---
|
||||
removeGroup(kdjSeriesRef.current, chart);
|
||||
if (indicators.kdj && bars.length > 0) {
|
||||
const { k, d, j } = calcKdj(bars);
|
||||
const mk = (color: string) =>
|
||||
chart.addSeries(
|
||||
LineSeries,
|
||||
{ color, lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
|
||||
4,
|
||||
);
|
||||
const kS = mk(KDJ_K);
|
||||
const dS = mk(KDJ_D);
|
||||
const jS = mk(KDJ_J);
|
||||
kS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: k[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
||||
dS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: d[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
||||
jS.setData(bars.map((_, i) => ({ time: bars[i].time as Time, value: j[i] })).filter((x) => x.value != null) as { time: Time; value: number }[]);
|
||||
kdjSeriesRef.current.push(kS, dS, jS);
|
||||
}
|
||||
}
|
||||
|
||||
// 模式切换:重建价格 series + 指标
|
||||
@@ -309,7 +291,7 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
data.map((d) => ({
|
||||
time: d.time as Time,
|
||||
value: d.volume,
|
||||
color: d.close >= d.open ? UP : DOWN,
|
||||
color: d.close >= d.open ? VOL_UP : VOL_DOWN,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -340,9 +322,6 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
|
||||
legend.push({ color: MACD_DIF, label: "DIF" }, { color: MACD_DEA, label: "DEA" });
|
||||
}
|
||||
if (indicators.rsi) legend.push({ color: RSI_COLOR, label: "RSI14" });
|
||||
if (indicators.kdj) {
|
||||
legend.push({ color: KDJ_K, label: "K" }, { color: KDJ_D, label: "D" }, { color: KDJ_J, label: "J" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
Reference in New Issue
Block a user