From 7b4fc1f8113440bdbdfdcfe43dde967138637e12 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 9 Jun 2026 22:18:18 +0900 Subject: [PATCH] =?UTF-8?q?obv=20=EC=B6=94=EA=B0=80=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradingChart.tsx | 1 + .../TradeSignalMiniChart.tsx | 12 +-- frontend/src/utils/ChartManager.ts | 86 ++++++++++++++----- frontend/src/utils/indicatorRegistry.ts | 1 + frontend/src/utils/indicatorSettingsEditor.ts | 7 +- 5 files changed, 79 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 92fa550..0a36560 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -1446,6 +1446,7 @@ const TradingChart: React.FC = ({ for (const ind of indicators) { mgr.applyIndicatorStyle(ind); } + void mgr.repairDualLineSeries(); syncIndicatorTrackingRefs(indicators); } else if ( removedIds.length > 0 diff --git a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx index f1d2604..a4b6bf2 100644 --- a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx +++ b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx @@ -54,7 +54,7 @@ const TradeSignalMiniChart: React.FC = ({ onRealtimeActivity, signalMarker, }) => { - const { getParams, getVisualConfig } = useIndicatorSettings(); + const { getParams, getVisualConfig, settingsLoaded, settingsRevision } = useIndicatorSettings(); const { defaults: appDefaults } = useAppSettings(); const appChartSource = (appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource; const managerRef = useRef(null); @@ -75,7 +75,7 @@ const TradeSignalMiniChart: React.FC = ({ getParams, getVisualConfig, ).map(enrichIndicatorConfig), - [indicators, getParams, getVisualConfig], + [indicators, getParams, getVisualConfig, settingsRevision], ); const needsDeepObv = useMemo( @@ -188,7 +188,7 @@ const TradeSignalMiniChart: React.FC = ({ return bars.some(b => (Number(b.volume) || 0) > 0); }, [needsDeepObv, bars]); - const chartDataReady = barsReady && obvBarsReady; + const chartDataReady = barsReady && obvBarsReady && settingsLoaded; useEffect(() => { if (!enabled || !chartDataReady) return; @@ -198,7 +198,7 @@ const TradeSignalMiniChart: React.FC = ({ setTimeout(() => void mgr.repairDualLineSeries(), delay), ); return () => timers.forEach(clearTimeout); - }, [enabled, chartDataReady, chartIndicators, market, timeframe]); + }, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision]); useLayoutEffect(() => { chartLiveReadyRef.current = false; @@ -279,7 +279,7 @@ const TradeSignalMiniChart: React.FC = ({ fillHeight ? 'tnl-mini-chart-canvas--fill' : '', ].filter(Boolean).join(' ')} > - {isLoading &&
로딩…
} + {(!settingsLoaded || isLoading) &&
로딩…
} {needsDeepObv && !isLoading && !obvBarsReady && (
OBV 거래량 데이터 로딩…
)} @@ -290,7 +290,7 @@ const TradeSignalMiniChart: React.FC = ({ )} {chartDataReady && ( i.id).join(',')}`} + key={`${market}-${timeframe}-${chartReloadTick}-${chartRealtimeSource}-${needsDeepObv ? 'obv' : 'std'}-${settingsRevision}-${chartIndicators.map(i => i.id).join(',')}`} chartVisible bars={bars} barsMarket={barsMarket} diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index fa3e328..b1c1c50 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -808,17 +808,27 @@ export class ChartManager { } } - /** 스타일 갱신·초기 추가 후 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; - if (this._dualLineMissingPlotIds(entry, config, def).length === 0) return; + /** plot0·plot1 시리즈 전부 제거 후 DB 설정 스타일로 재생성 */ + private async _rebuildDualLinePlotSeriesForEntry( + entry: IndicatorEntry, + config: IndicatorConfig, + def: NonNullable>, + ): Promise { if (this.rawBars.length === 0) return; const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2; const indicatorHidden = !this._isIndicatorEffectivelyVisible(config); + + for (const pl of entry.hlineRefs) { + try { entry.seriesList[0]?.removePriceLine(pl); } catch { /* ok */ } + } + entry.hlineRefs = []; + for (const s of entry.seriesList) { + try { this.chart.removeSeries(s); } catch { /* ok */ } + } + entry.seriesList = []; + entry.seriesMeta = []; + try { const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params); this._appendMissingPlotSeriesToLists( @@ -838,6 +848,30 @@ export class ChartManager { const actualPane = this._readSeriesPaneIndex(entry.seriesList[0]); if (actualPane >= 2) entry.paneIndex = actualPane; } + } catch (e) { + console.error(`[Indicator] ${config.type} dual-line rebuild error:`, e); + } + } + + /** 스타일 갱신·초기 추가 후 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; + if (this.rawBars.length === 0) return; + + const missing = this._dualLineMissingPlotIds(entry, config, def); + if (missing.length > 0) { + await this._rebuildDualLinePlotSeriesForEntry(entry, config, def); + return; + } + + if (entry.seriesList.length === 0) return; + try { + const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params); + this._setIndicatorPlotsFullData(entry, result.plots as Record, config); + entry.config = config; } catch (e) { console.error(`[Indicator] ${config.type} dual-line repair error:`, e); } @@ -1063,20 +1097,26 @@ export class ChartManager { this._notifyPaneLayout(); } - /** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 + 전체 plot setData */ + /** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 + 전체 plot setData + 스타일(DB) 반영 */ async repairDualLineSeries(): Promise { for (const entry of this.indicators.values()) { if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type) || !entry.config) continue; - await this._ensureDualLinePlotSeriesForEntry(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 */ } + const config = enrichIndicatorConfig(entry.config); + const def = getIndicatorDef(entry.type); + if (!def) continue; + + const missing = this._dualLineMissingPlotIds(entry, config, def); + if (missing.length > 0) { + await this._rebuildDualLinePlotSeriesForEntry(entry, config, def); + } else if (entry.seriesList.length > 0 && this.rawBars.length > 0) { + try { + const result = await calculateIndicator( + config.type, this.rawBars.slice(), config.params ?? {}, + ); + this._setIndicatorPlotsFullData(entry, result.plots as Record, config); + } catch { /* ok */ } + } + this.applyIndicatorStyle(config); } } @@ -2902,7 +2942,7 @@ export class ChartManager { * 인디케이터의 스타일(색상·선폭·plotVisibility)을 in-place 로 일괄 적용. * 시리즈를 제거·재생성하지 않으므로 pane 재번호 문제가 없음. */ - applyIndicatorStyle(config: IndicatorConfig): void { + applyIndicatorStyle(config: IndicatorConfig, options?: { skipDualLineRefresh?: boolean }): void { config = enrichIndicatorConfig(config); const entry = this.indicators.get(config.id); if (!entry) return; @@ -3035,8 +3075,12 @@ export class ChartManager { void this._repaintHistogramSeries(config.id, config); } - if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) { - void this._ensureDualLinePlotSeriesForEntry(entry); + if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !options?.skipDualLineRefresh) { + void this._ensureDualLinePlotSeriesForEntry(entry).then(() => { + if (entry.config) { + this.applyIndicatorStyle(enrichIndicatorConfig(entry.config), { skipDualLineRefresh: true }); + } + }); } if (this._candleOnlyLayout && this._isSubPaneIndicator(entry)) { diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index bb7a3df..4be8cb8 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -453,6 +453,7 @@ function normalizeDualLinePlotDef(saved: PlotDef, reg: PlotDef): PlotDef { title: reg.title, type: 'line', lineWidth: Math.max(1, saved.lineWidth ?? reg.lineWidth ?? 1), + lineStyle: saved.lineStyle ?? reg.lineStyle ?? 'solid', color: resolvePlotLineColor(saved.color, reg.color), }; } diff --git a/frontend/src/utils/indicatorSettingsEditor.ts b/frontend/src/utils/indicatorSettingsEditor.ts index 2525f84..a65b2b0 100644 --- a/frontend/src/utils/indicatorSettingsEditor.ts +++ b/frontend/src/utils/indicatorSettingsEditor.ts @@ -64,10 +64,15 @@ export function initializeIndicatorConfigForEditor( : { ...raw.params }; const visual = getVisualConfig?.(raw.type, def.plots, def.hlines); + const mergedPlots = mergePlotDefs( + raw.plots?.length ? raw.plots : undefined, + mergePlotDefs(visual?.plots, def.plots), + ); + return enrichIndicatorConfig({ ...raw, params, - plots: raw.plots?.length ? raw.plots : visual?.plots, + plots: mergedPlots, hlines: raw.hlines?.length ? raw.hlines : visual?.hlines, cloudColors: raw.cloudColors ?? visual?.cloudColors, bandBackground: raw.bandBackground ?? visual?.bandBackground,