diff --git a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx index edbb077..f1d2604 100644 --- a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx +++ b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx @@ -123,7 +123,12 @@ const TradeSignalMiniChart: React.FC = ({ } syncChartLayout(); applySignalMarkers(); - void mgr?.repairDualLineSeries(); + if (!mgr) return; + [0, 100, 500, 1200].forEach(delay => { + setTimeout(() => { + void managerRef.current?.repairDualLineSeries(); + }, delay); + }); }, [syncChartLayout, applySignalMarkers]); const handleManagerReady = useCallback((mgr: ChartManager) => { @@ -185,6 +190,16 @@ const TradeSignalMiniChart: React.FC = ({ const chartDataReady = barsReady && obvBarsReady; + useEffect(() => { + if (!enabled || !chartDataReady) return; + const mgr = managerRef.current; + if (!mgr?.hasMainSeries()) return; + const timers = [200, 800, 1600].map(delay => + setTimeout(() => void mgr.repairDualLineSeries(), delay), + ); + return () => timers.forEach(clearTimeout); + }, [enabled, chartDataReady, chartIndicators, market, timeframe]); + useLayoutEffect(() => { chartLiveReadyRef.current = false; pendingBarRef.current = null; diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 35b60ce..fa3e328 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -748,23 +748,18 @@ export class ChartManager { return loadGen !== this._dataGeneration; } - /** OBV — RSI/CCI와 동일한 공유 price scale, 이전 autoscale 오버라이드 제거 */ - private _resetObvSharedScale(entry: IndicatorEntry): void { - if (entry.type !== 'OBV') return; - for (const s of entry.seriesList) { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (s as ISeriesApi).applyOptions({ autoscaleInfoProvider: undefined }); - } catch { /* ok */ } - } - const anchor = entry.seriesList[0]; - if (!anchor) return; - try { - anchor.priceScale().applyOptions({ - visible: true, - scaleMargins: { top: 0.1, bottom: 0.1 }, - }); - } catch { /* ok */ } + /** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */ + private _dualLineMissingPlotIds( + entry: IndicatorEntry, + config: IndicatorConfig, + def: ReturnType | undefined, + ): string[] { + const plotDefs = config.plots ?? def?.plots ?? []; + const existing = new Set(entry.seriesMeta.map(m => m.plotId)); + return plotDefs + .filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false) + .map(p => p.id) + .filter(id => !existing.has(id)); } private _appendMissingPlotSeriesToLists( @@ -813,17 +808,13 @@ export class ChartManager { } } - /** 스타일 갱신·초기 추가 후 OBV 등 본선(plot0) 시리즈 누락 보정 */ + /** 스타일 갱신·초기 추가 후 OBV 등 본선(plot0) 시리즈 누락 보정 + 전체 setData */ private async _ensureDualLinePlotSeriesForEntry(entry: IndicatorEntry): Promise { if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) return; const config = enrichIndicatorConfig(entry.config!); const def = getIndicatorDef(config.type); if (!def) return; - const plotDefs = config.plots ?? def.plots ?? []; - const expectedCount = plotDefs.filter( - p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false, - ).length; - if (entry.seriesList.length >= expectedCount) return; + if (this._dualLineMissingPlotIds(entry, config, def).length === 0) return; if (this.rawBars.length === 0) return; const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2; @@ -839,8 +830,8 @@ export class ChartManager { indicatorHidden, def, ); - if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) { - this._resetObvSharedScale(entry); + if (entry.seriesList.length > 0) { + this._setIndicatorPlotsFullData(entry, result.plots as Record, config); } entry.config = config; if (entry.seriesList.length > 0) { @@ -952,12 +943,12 @@ export class ChartManager { ); } - if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) { - const obvEntry: IndicatorEntry = { + if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) { + const dualEntry: IndicatorEntry = { id: config.id, type: config.type, seriesList, seriesMeta, paneIndex: pane, alertLines: [], hlineRefs: [], config, }; - this._resetObvSharedScale(obvEntry); + this._setIndicatorPlotsFullData(dualEntry, result.plots as Record, config); } // hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지) @@ -1013,6 +1004,9 @@ export class ChartManager { this.indicators.set(config.id, entry); if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) { await this._ensureDualLinePlotSeriesForEntry(entry); + if (entry.seriesList.length > 0 && !indicatorHidden) { + this._setIndicatorPlotsFullData(entry, result.plots as Record, config); + } } this.applyIndicatorStyle(config); if (config.type === 'BollingerBands' && !indicatorHidden) { @@ -1069,15 +1063,20 @@ export class ChartManager { this._notifyPaneLayout(); } - /** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 (미니 차트·스타일 갱신 후) */ + /** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 + 전체 plot setData */ async repairDualLineSeries(): Promise { for (const entry of this.indicators.values()) { - if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue; + if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type) || !entry.config) continue; await this._ensureDualLinePlotSeriesForEntry(entry); - if (entry.config) this.applyIndicatorStyle(entry.config); - if (entry.type === 'OBV' && entry.seriesList.length > 0) { - this._resetObvSharedScale(entry); - } + if (entry.seriesList.length === 0 || this.rawBars.length === 0) continue; + try { + const config = enrichIndicatorConfig(entry.config); + const result = await calculateIndicator( + config.type, this.rawBars.slice(), config.params ?? {}, + ); + this._setIndicatorPlotsFullData(entry, result.plots as Record, config); + this.applyIndicatorStyle(config); + } catch { /* ok */ } } } @@ -1087,6 +1086,7 @@ export class ChartManager { await this.addIndicator(config, { skipLayout: true }); } if (configs.length > 0) { + await this.repairDualLineSeries(); this._finalizeIndicatorPaneLayout(); this._refreshAllIchimokuClouds(); this.syncChartOverlayVisibility(); @@ -1876,8 +1876,17 @@ export class ChartManager { for (const [, entry] of this.indicators.entries()) { if (dataGenAtStart !== this._dataGeneration) return; - if (!entry.config || entry.seriesList.length === 0) continue; + if (!entry.config) continue; try { + if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) { + const config = enrichIndicatorConfig(entry.config); + const def = getIndicatorDef(entry.type); + if (this._dualLineMissingPlotIds(entry, config, def).length > 0) { + await this._ensureDualLinePlotSeriesForEntry(entry); + } + } + if (entry.seriesList.length === 0) continue; + const { plots } = await calculateIndicator( entry.config.type, barsSnapshot, entry.config.params ?? {} ); @@ -2099,8 +2108,17 @@ export class ChartManager { 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; + if (!entry.config) continue; try { + if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) { + const config = enrichIndicatorConfig(entry.config); + const def = getIndicatorDef(entry.type); + if (this._dualLineMissingPlotIds(entry, config, def).length > 0) { + await this._ensureDualLinePlotSeriesForEntry(entry); + } + } + if (entry.seriesList.length === 0) continue; + const { plots } = await calculateIndicator( entry.config.type, barsSnapshot, entry.config.params ?? {} ); @@ -2984,10 +3002,6 @@ export class ChartManager { } } - if (entry.type === 'OBV' && entry.seriesList.length > 0) { - this._resetObvSharedScale(entry); - } - if (entry.type === 'BollingerBands') { detachBbBandFill(entry); if (this._shouldShowBbBandFillForConfig(config)) { diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index a18db6f..bb7a3df 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -479,7 +479,10 @@ function normalizeSignalLineIndicatorPlots( const order = ['plot0', 'plot1']; return order.indexOf(a.id) - order.indexOf(b.id); }); - return { plots: nextPlots, plotVisibility: { ...plotVisibility } }; + const nextVis = { ...plotVisibility }; + if (nextVis.plot0 !== false) nextVis.plot0 = true; + if (nextVis.plot1 !== false) nextVis.plot1 = true; + return { plots: nextPlots, plotVisibility: nextVis }; } const regSignal = registryPlots.find(p => p.id === 'plot1');