종목변경 시 캔들차트 사라지는 현상 수정

This commit is contained in:
Macbook
2026-05-24 21:51:27 +09:00
parent af230a4233
commit 3c66c562d8
14 changed files with 288 additions and 219 deletions
+38 -20
View File
@@ -164,6 +164,18 @@ export class ChartManager {
private drawingLines: IPriceLine[] = [];
private _clickHandler: ((p: MouseEventParams<Time>) => void) | null = null;
private rawBars: OHLCVBar[] = [];
/** setData 호출 세대 — addIndicator 중 stale rawBars 적용 방지 */
private _dataGeneration = 0;
/** 진행 중인 보조지표 스로틀 갱신 취소 (종목 전환·재로드 시) */
cancelPendingIndicatorUpdates(): void {
if (this._indUpdateTimer) {
clearTimeout(this._indUpdateTimer);
this._indUpdateTimer = null;
}
this._pendingIndUpdate = false;
this._indRunning = false;
}
private currentTheme: Theme = 'dark';
private currentChartType: ChartType = 'candlestick';
private displayTimezone = DEFAULT_DISPLAY_TIMEZONE;
@@ -258,12 +270,8 @@ export class ChartManager {
setData(bars: OHLCVBar[], chartType: ChartType, theme: Theme): void {
// 전체 재로드 전: 진행 중인 지표 갱신 타이머·플래그를 초기화해
// 이전 rawBars 기반의 stale 계산이 새 지표에 적용되는 것을 방지한다.
if (this._indUpdateTimer) {
clearTimeout(this._indUpdateTimer);
this._indUpdateTimer = null;
}
this._pendingIndUpdate = false;
this._indRunning = false;
this.cancelPendingIndicatorUpdates();
this._dataGeneration += 1;
this.rawBars = bars;
this.currentTheme = theme;
@@ -407,6 +415,7 @@ export class ChartManager {
async addIndicator(config: IndicatorConfig): Promise<void> {
config = enrichIndicatorConfig(config);
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
const dataGenAtStart = this._dataGeneration;
const indicatorHidden = config.hidden === true;
const def = getIndicatorDef(config.type);
const seriesList: ISeriesApi<SeriesType>[] = [];
@@ -416,6 +425,8 @@ export class ChartManager {
// 스냅샷 전달: await 중 rawBars 가 변경돼도 이 지표는 일관된 초기 데이터로 setData
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
if (dataGenAtStart !== this._dataGeneration) return;
if (def?.returnsMarkers && result.markers && result.markers.length > 0) {
this.patternMarkers.push({ id: config.id, markers: result.markers });
this._reapplyAllPatternMarkers();
@@ -427,10 +438,7 @@ export class ChartManager {
// ─── 일목균형표 특별 처리 ────────────────────────────────────────────
if (config.type === 'IchimokuCloud') {
const cloudPlugin = await this._addIchimokuSeries(result, pane, config, seriesList);
// 일목균형표 5라인 plotId: plot0~plot4 순서 고정
const ichiPlotIds = ['plot0', 'plot1', 'plot2', 'plot3', 'plot4'];
const ichiMeta = seriesList.map((_, idx) => ({ plotId: ichiPlotIds[idx] ?? `plot${idx}`, isHistogram: false, color: '' }));
const { cloudPlugin, seriesMeta: ichiMeta } = await this._addIchimokuSeries(result, pane, config, seriesList);
this.indicators.set(config.id, { id: config.id, type: config.type, seriesList, seriesMeta: ichiMeta, paneIndex: pane, alertLines: [], hlineRefs: [], config, cloudPlugin });
this.applyIndicatorStyle(config);
return;
@@ -717,9 +725,10 @@ export class ChartManager {
pane: number,
_config: IndicatorConfig,
seriesList: ISeriesApi<SeriesType>[],
): Promise<IchimokuCloudPlugin> {
): Promise<{ cloudPlugin: IchimokuCloudPlugin; seriesMeta: IndicatorEntry['seriesMeta'] }> {
const displacement = (_config.params?.displacement as number) ?? 26;
const laggingPeriod = (_config.params?.laggingSpan2Periods as number) ?? 52;
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
const plotInfo = [
{ id: 'plot0', title: getIchimokuPlotTitle('plot0'), color: '#26c6da', lastVal: true },
@@ -763,6 +772,7 @@ export class ChartManager {
}, pane);
series.setData(seriesData);
seriesList.push(series);
seriesMeta.push({ plotId: info.id, isHistogram: false, color: info.color });
if (info.id === 'plot3') spanASeriesRef = series;
}
@@ -775,7 +785,7 @@ export class ChartManager {
host.attachPrimitive(cloudPlugin);
this._applyIchimokuCloudPlugin(cloudPlugin, result.plots, _config);
}
return cloudPlugin;
return { cloudPlugin, seriesMeta };
}
/** 선행스팬A의 미래 26봉: (전환선 + 기준선) / 2, 미래 타임스탬프로 매핑 */
@@ -989,20 +999,24 @@ export class ChartManager {
/** 모든 보조지표 시리즈의 마지막 데이터 포인트를 현재 rawBars 기준으로 재계산·갱신 */
private async _updateIndicatorLastPoints(): Promise<void> {
if (this.rawBars.length === 0) return;
const dataGenAtStart = this._dataGeneration;
// 루프 시작 전 스냅샷 고정: 루프 실행 중 rawBars 가 변경돼도 모든 지표가
// 동일한 데이터 기준으로 계산되어 지표 간 시각적 불일치를 방지한다.
const barsSnapshot = this.rawBars.slice();
for (const [, entry] of this.indicators.entries()) {
if (dataGenAtStart !== this._dataGeneration) return;
if (!entry.config || entry.seriesList.length === 0) continue;
try {
const { plots } = await calculateIndicator(
entry.config.type, barsSnapshot, entry.config.params ?? {}
);
// 일목균형표: 구름 플러그인 + 미래 프로젝션 라인 모두 갱신
if (entry.type === 'IchimokuCloud' && entry.cloudPlugin) {
this._applyIchimokuCloudPlugin(entry.cloudPlugin, plots, entry.config);
if (dataGenAtStart !== this._dataGeneration) return;
// 일목균형표: 선행스팬 미래 구간 포함 전체 setData (틱 갱신 시에도 유지)
if (entry.type === 'IchimokuCloud') {
this._refreshIchimokuSeriesData(entry, plots);
continue;
}
this._applyLastPointsToSeries(entry, plots, /* updateFutureSpans */ false);
} catch { /* 계산 실패 무시 */ }
@@ -1134,15 +1148,12 @@ export class ChartManager {
*/
async appendBar(bar: OHLCVBar): Promise<void> {
if (!this.mainSeries) return;
const dataGenAtStart = this._dataGeneration;
const t = getTheme(this.currentTheme);
// 새 캔들 추가 시 이 함수 내부에서 모든 지표를 직접 재계산하므로,
// updateBar 가 예약한 스로틀 타이머를 취소해 중복 계산을 방지합니다.
if (this._indUpdateTimer) {
clearTimeout(this._indUpdateTimer);
this._indUpdateTimer = null;
this._pendingIndUpdate = false;
}
this.cancelPendingIndicatorUpdates();
// rawBars에 신규 바 추가 (중복 방지)
const last = this.rawBars[this.rawBars.length - 1];
@@ -1181,11 +1192,13 @@ export class ChartManager {
// rawBars 스냅샷을 찍어 루프 도중 rawBars 가 변경돼도 일관된 계산 보장
const barsSnapshot = this.rawBars.slice();
for (const [, entry] of this.indicators.entries()) {
if (dataGenAtStart !== this._dataGeneration) return;
if (!entry.config || entry.seriesList.length === 0) continue;
try {
const { plots } = await calculateIndicator(
entry.config.type, barsSnapshot, entry.config.params ?? {}
);
if (dataGenAtStart !== this._dataGeneration) return;
// 일목균형표: 구름 플러그인 + SpanA/B 미래 라인 갱신
if (entry.type === 'IchimokuCloud' && entry.cloudPlugin) {
this._applyIchimokuCloudPlugin(entry.cloudPlugin, plots, entry.config);
@@ -2356,6 +2369,11 @@ export class ChartManager {
return this.mainSeries !== null && this.rawBars.length > 0;
}
/** 로드된 보조지표 수 (종목 전환 후 캔들만 그려진 상태 감지용) */
getIndicatorCount(): number {
return this.indicators.size;
}
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
getRawBarsLength(): number {
return this.rawBars.length;