Skip to content

Commit 002dad0

Browse files
committed
refactor: 更新 K 线聚合与加载逻辑,支持 1s 数据处理
- 修改 CandleAggregator 和 MarketEngine 的方法名称,从 `load_history_1m_and_aggregate` 更新为 `load_history_1s_and_aggregate`,以反映新的数据粒度 - 调整 K 线聚合逻辑,确保能够接收并处理 1s K 线数据,自动聚合到各个高周期 (1m/5m/15m/1H/4H/1D) - 更新相关文档与类型定义,确保与新方法一致 - 优化图表组件的逻辑,确保在高频数据更新下的稳定性与性能
1 parent 18f1246 commit 002dad0

9 files changed

Lines changed: 496 additions & 146 deletions

File tree

PRD.md

Lines changed: 386 additions & 100 deletions
Large diffs are not rendered by default.

core/src/engine/data/candles.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ impl CandleAggregator {
109109
///
110110
/// 接收 1s K 线数据,自动聚合到 1m/5m/15m/1H/4H/1D
111111
/// 返回各周期加载的 K 线数量
112-
pub fn aggregate_history_from_1m(
112+
pub fn aggregate_history_from_1s(
113113
candle_cache: &mut HashMap<Timeframe, CandleCache>,
114114
candles_s1: Vec<Candle>,
115115
) -> Vec<(String, usize)> {

core/src/engine/market_engine/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ impl MarketEngine {
170170
///
171171
/// @param candles_js - 1s K 线数组 (JS 格式)
172172
/// @returns 各周期加载的 K 线数量
173-
pub fn load_history_1m_and_aggregate(&mut self, candles_js: JsValue) -> Result<JsValue, JsValue> {
173+
pub fn load_history_1s_and_aggregate(&mut self, candles_js: JsValue) -> Result<JsValue, JsValue> {
174174
let candles: Vec<Candle> = from_js!(candles_js, Vec<Candle>, "解析 1s K 线失败")?;
175175

176176
if candles.is_empty() {
@@ -184,7 +184,7 @@ impl MarketEngine {
184184
}
185185

186186
// 聚合到所有周期
187-
let results = CandleAggregator::aggregate_history_from_1m(&mut self.candle_cache, candles);
187+
let results = CandleAggregator::aggregate_history_from_1s(&mut self.candle_cache, candles);
188188

189189
to_js!(results)
190190
}

src/components/Dashboard/Chart/LightweightChart/UnifiedMultiPaneChart.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ const UnifiedMultiPaneChart = forwardRef<UnifiedMultiPaneChartHandle, UnifiedMul
6666

6767
// === Hook: Chart Setup ===
6868
const {
69-
chart,
7069
chartEpoch,
7170
mainSeriesRefs,
7271
subSeriesRefs,
@@ -84,7 +83,6 @@ const UnifiedMultiPaneChart = forwardRef<UnifiedMultiPaneChartHandle, UnifiedMul
8483

8584
// === Hook: Candle Series Data ===
8685
useCandleSeriesData({
87-
chart,
8886
candles,
8987
mainSeriesRefs,
9088
chartEpoch,
@@ -93,12 +91,12 @@ const UnifiedMultiPaneChart = forwardRef<UnifiedMultiPaneChartHandle, UnifiedMul
9391

9492
// === Hook: Main Indicator Series (MA/EMA/BOLL) ===
9593
useMainIndicatorSeries({
96-
chart,
9794
candles,
9895
indicatorData,
9996
activeMainIndicators,
10097
mainSeriesRefs,
10198
chartEpoch,
99+
getSafeChart,
102100
});
103101

104102
// === Hook: Sub Pane Series (VOL/MACD/RSI) ===

src/components/Dashboard/Chart/LightweightChart/hooks/useCandleSeriesData.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@ import { candlesToChartData } from '../utils/dataTransform';
1111
import type { MainSeriesRefs } from './useUnifiedChartSetup';
1212

1313
export interface UseCandleSeriesDataOptions {
14-
chart: IChartApi | null;
1514
candles: Candle[];
1615
mainSeriesRefs: React.RefObject<MainSeriesRefs>;
1716
chartEpoch: number;
1817
getSafeChart: () => IChartApi | null;
1918
}
2019

2120
export function useCandleSeriesData({
22-
chart,
2321
candles,
2422
mainSeriesRefs,
2523
chartEpoch,
@@ -29,14 +27,16 @@ export function useCandleSeriesData({
2927

3028
// Update candle data
3129
useEffect(() => {
32-
if (!chart || candles.length === 0) return;
30+
// 使用 getSafeChart 确保 chart 未被销毁
31+
const safeChart = getSafeChart();
32+
if (!safeChart || candles.length === 0) return;
3333
const refs = mainSeriesRefs.current;
3434
if (!refs) return;
3535
const candleSeries = refs.candle;
3636
if (!candleSeries) return;
3737

3838
try { candleSeries.setData(candlesToChartData(candles)); } catch { /* ignore */ }
39-
}, [chart, candles, mainSeriesRefs, chartEpoch]);
39+
}, [candles, mainSeriesRefs, chartEpoch, getSafeChart]);
4040

4141
// Fit content once after chart creation
4242
useEffect(() => {

src/components/Dashboard/Chart/LightweightChart/hooks/useMainIndicatorSeries.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,65 @@
55
*/
66

77
import { useEffect } from 'react';
8-
import { LineSeries, LineStyle, type IChartApi } from 'lightweight-charts';
8+
import { LineSeries, LineStyle, type IChartApi, type ISeriesApi } from 'lightweight-charts';
99
import type { Candle, IndicatorData } from '../../../../../types';
1010
import { CHART_COLORS } from '../utils/chartColors';
1111
import { indicatorToLineData } from '../utils/dataTransform';
1212
import type { MainSeriesRefs } from './useUnifiedChartSetup';
1313

1414
export interface UseMainIndicatorSeriesOptions {
15-
chart: IChartApi | null;
1615
candles: Candle[];
1716
indicatorData: IndicatorData;
1817
activeMainIndicators: string[];
1918
mainSeriesRefs: React.RefObject<MainSeriesRefs>;
2019
chartEpoch: number;
20+
getSafeChart: () => IChartApi | null;
2121
}
2222

2323
export function useMainIndicatorSeries({
24-
chart,
2524
candles,
2625
indicatorData,
2726
activeMainIndicators,
2827
mainSeriesRefs,
2928
chartEpoch,
29+
getSafeChart,
3030
}: UseMainIndicatorSeriesOptions): void {
3131
useEffect(() => {
32+
// 获取安全的 chart 引用
33+
const chart = getSafeChart();
3234
if (!chart || candles.length === 0) return;
35+
3336
const refs = mainSeriesRefs.current;
3437
if (!refs || !refs.candle) return;
3538

39+
/**
40+
* 安全地添加 series
41+
* 在 HMR 期间,chart 可能在 addSeries 调用后、渲染前被销毁
42+
*/
43+
const safeAddSeries = (
44+
options: Parameters<typeof chart.addSeries>[1],
45+
paneIndex: number
46+
): ISeriesApi<'Line'> | null => {
47+
const c = getSafeChart();
48+
if (!c) return null;
49+
try {
50+
return c.addSeries(LineSeries, options, paneIndex);
51+
} catch {
52+
return null;
53+
}
54+
};
55+
56+
/**
57+
* 安全地移除 series
58+
*/
59+
const safeRemoveSeries = (series: ISeriesApi<'Line'>) => {
60+
const c = getSafeChart();
61+
if (!c) return;
62+
try {
63+
c.removeSeries(series);
64+
} catch { /* ignore */ }
65+
};
66+
3667
// === MA ===
3768
const wantMA = activeMainIndicators.includes('MA');
3869
const ma7Values = indicatorData.ma7.length === candles.length
@@ -52,24 +83,27 @@ export function useMainIndicatorSeries({
5283
];
5384

5485
if (!wantMA) {
55-
refs.ma.forEach((s) => {
56-
try { chart.removeSeries(s); } catch { /* ignore */ }
57-
});
86+
refs.ma.forEach((s) => safeRemoveSeries(s));
5887
refs.ma.clear();
5988
} else {
6089
maConfigs.forEach(({ key, color, values }) => {
6190
let s = refs.ma.get(key);
6291
if (!s) {
63-
s = chart.addSeries(LineSeries, {
92+
const newSeries = safeAddSeries({
6493
color,
6594
lineWidth: 1,
6695
priceScaleId: 'right',
6796
lastValueVisible: false,
6897
priceLineVisible: false,
6998
}, 0);
70-
refs.ma.set(key, s);
99+
if (newSeries) {
100+
s = newSeries;
101+
refs.ma.set(key, s);
102+
}
103+
}
104+
if (s) {
105+
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
71106
}
72-
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
73107
});
74108
}
75109

@@ -81,24 +115,27 @@ export function useMainIndicatorSeries({
81115
];
82116

83117
if (!wantEMA) {
84-
refs.ema.forEach((s) => {
85-
try { chart.removeSeries(s); } catch { /* ignore */ }
86-
});
118+
refs.ema.forEach((s) => safeRemoveSeries(s));
87119
refs.ema.clear();
88120
} else {
89121
emaConfigs.forEach(({ key, color, values }) => {
90122
let s = refs.ema.get(key);
91123
if (!s) {
92-
s = chart.addSeries(LineSeries, {
124+
const newSeries = safeAddSeries({
93125
color,
94126
lineWidth: 1,
95127
priceScaleId: 'right',
96128
lastValueVisible: false,
97129
priceLineVisible: false,
98130
}, 0);
99-
refs.ema.set(key, s);
131+
if (newSeries) {
132+
s = newSeries;
133+
refs.ema.set(key, s);
134+
}
135+
}
136+
if (s) {
137+
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
100138
}
101-
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
102139
});
103140
}
104141

@@ -111,15 +148,13 @@ export function useMainIndicatorSeries({
111148
];
112149

113150
if (!wantBOLL) {
114-
refs.boll.forEach((s) => {
115-
try { chart.removeSeries(s); } catch { /* ignore */ }
116-
});
151+
refs.boll.forEach((s) => safeRemoveSeries(s));
117152
refs.boll.clear();
118153
} else {
119154
bollConfigs.forEach(({ key, color, values, lineStyle }) => {
120155
let s = refs.boll.get(key);
121156
if (!s) {
122-
s = chart.addSeries(LineSeries, {
157+
const newSeries = safeAddSeries({
123158
color,
124159
lineWidth: 1,
125160
lineStyle,
@@ -128,10 +163,15 @@ export function useMainIndicatorSeries({
128163
lastValueVisible: false,
129164
priceLineVisible: false,
130165
}, 0);
131-
refs.boll.set(key, s);
166+
if (newSeries) {
167+
s = newSeries;
168+
refs.boll.set(key, s);
169+
}
170+
}
171+
if (s) {
172+
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
132173
}
133-
try { s.setData(indicatorToLineData(candles, values)); } catch { /* ignore */ }
134174
});
135175
}
136-
}, [chart, candles, indicatorData, activeMainIndicators, mainSeriesRefs, chartEpoch]);
176+
}, [candles, indicatorData, activeMainIndicators, mainSeriesRefs, chartEpoch, getSafeChart]);
137177
}

src/components/Dashboard/Chart/LightweightChart/hooks/useUnifiedChartSetup.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ export function useUnifiedChartSetup({
170170

171171
// Create chart
172172
const chart = createChart(containerRef.current, {
173-
attributionLogo: false,
174173
layout: {
175174
background: { type: ColorType.Solid, color: CHART_COLORS.BACKGROUND },
176175
textColor: CHART_COLORS.TEXT_SECONDARY,
@@ -346,22 +345,44 @@ export function useUnifiedChartSetup({
346345
};
347346
try { chart.timeScale().subscribeVisibleLogicalRangeChange(onRange); } catch { /* ignore */ }
348347

349-
// ResizeObserver
350-
const resizeObserver = new ResizeObserver(() => {
348+
// 执行 resize 的核心逻辑
349+
const doResize = () => {
350+
if (disposedRef.current) return;
351+
const el = containerRef.current;
352+
if (!el) return;
353+
try {
354+
chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
355+
computePaneOffsets(chart, panePlan);
356+
} catch { /* ignore */ }
357+
};
358+
359+
// ResizeObserver 的 resize 处理(使用 rAF 防抖)
360+
const handleResizeObserver = () => {
351361
if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
352362
rafIdRef.current = requestAnimationFrame(() => {
353363
rafIdRef.current = null;
354-
if (disposedRef.current) return;
355-
const el = containerRef.current;
356-
if (!el) return;
357-
try {
358-
chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
359-
computePaneOffsets(chart, panePlan);
360-
} catch { /* ignore */ }
364+
doResize();
361365
});
362-
});
366+
};
367+
368+
// Window resize 的处理(使用 setTimeout 防抖,等待布局稳定)
369+
let windowResizeTimer: ReturnType<typeof setTimeout> | null = null;
370+
const handleWindowResize = () => {
371+
if (windowResizeTimer !== null) clearTimeout(windowResizeTimer);
372+
// 响应式断点切换时,CSS 布局需要时间稳定,延迟 150ms 后再更新
373+
windowResizeTimer = setTimeout(() => {
374+
windowResizeTimer = null;
375+
doResize();
376+
}, 150);
377+
};
378+
379+
// ResizeObserver: 监听容器尺寸变化(直接响应)
380+
const resizeObserver = new ResizeObserver(handleResizeObserver);
363381
resizeObserver.observe(containerRef.current);
364382

383+
// Window resize: 处理响应式断点切换(延迟响应)
384+
window.addEventListener('resize', handleWindowResize);
385+
365386
setChartEpoch((x) => x + 1);
366387

367388
return () => {
@@ -370,7 +391,12 @@ export function useUnifiedChartSetup({
370391
cancelAnimationFrame(rafIdRef.current);
371392
rafIdRef.current = null;
372393
}
394+
if (windowResizeTimer !== null) {
395+
clearTimeout(windowResizeTimer);
396+
windowResizeTimer = null;
397+
}
373398
resizeObserver.disconnect();
399+
window.removeEventListener('resize', handleWindowResize);
374400
try { chart.unsubscribeCrosshairMove(onMove); } catch { /* ignore */ }
375401
try { chart.timeScale().unsubscribeVisibleLogicalRangeChange(onRange); } catch { /* ignore */ }
376402
try { chart.remove(); } catch { /* ignore */ }

src/hooks/useWasmEngine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,9 @@ export function useWasmEngine(tickInterval: number = 100): UseWasmEngineReturn {
341341
try {
342342
const startTime = performance.now();
343343

344-
// 加载 1m K 线并自动聚合到所有高周期 (5m/15m/1H/4H/1D)
344+
// 加载 1s K 线并自动聚合到所有高周期 (1m/5m/15m/1H/4H/1D)
345345
const results =
346-
engineRef.current.load_history_1m_and_aggregate(historyCandles);
346+
engineRef.current.load_history_1s_and_aggregate(historyCandles);
347347
historyLoadedRef.current = true;
348348

349349
const loadTime = performance.now() - startTime;

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export interface MarketEngineInstance {
286286
candles: HistoryCandle[],
287287
) => number;
288288
/** 加载 1s K 线并自动聚合到所有高周期 (1m/5m/15m/1H/4H/1D) */
289-
load_history_1m_and_aggregate: (
289+
load_history_1s_and_aggregate: (
290290
candles: HistoryCandle[],
291291
) => [string, number][];
292292

0 commit comments

Comments
 (0)