종목변경 시 캔들차트 사라지는 현상 수정
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user