diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index b8b1e78..60a26ad 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -739,6 +739,41 @@ export class ChartManager { } /** 이중 plot 지표 — addIndicator 루프에서 누락된 plot 시리즈 추가 */ + /** + * OBV pane y축 스케일을 최근 N봉 실제 값 기준으로 강제 고정. + * 과거에 OBV가 큰 값을 기록했어도 현재 구간이 y축 하단에 압축되지 않도록 방지. + */ + private _applyObvTightScale( + seriesList: ISeriesApi[], + plots: Record, + recentCount = 60, + ): void { + const allRecent: number[] = []; + for (const key of ['plot0', 'plot1']) { + const pts = (plots[key] ?? []).filter(p => p.value != null && isFinite(p.value as number)); + const slice = pts.slice(-recentCount).map(p => p.value as number); + allRecent.push(...slice); + } + if (allRecent.length === 0) return; + const minV = Math.min(...allRecent); + const maxV = Math.max(...allRecent); + const span = Math.max(1, maxV - minV); + const pad = span * 0.3; + const tMin = minV - pad; + const tMax = maxV + pad; + for (const s of seriesList) { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (s as ISeriesApi).applyOptions({ + autoscaleInfoProvider: () => ({ + priceRange: { minValue: tMin, maxValue: tMax }, + margins: { above: 0, below: 0 }, + }), + }); + } catch { /* ok */ } + } + } + private _appendMissingPlotSeriesToLists( seriesList: ISeriesApi[], seriesMeta: IndicatorEntry['seriesMeta'], @@ -812,6 +847,9 @@ export class ChartManager { indicatorHidden, def, ); + if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) { + this._applyObvTightScale(entry.seriesList, result.plots as Record); + } entry.config = config; if (entry.seriesList.length > 0) { const actualPane = this._readSeriesPaneIndex(entry.seriesList[0]); @@ -911,14 +949,6 @@ export class ChartManager { }, pane); series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value }))); seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color }); - - // OBV 디버그: 마지막 5개 값 출력 - if (config.type === 'OBV') { - const last5 = filtered.slice(-5).map(p => p.value.toFixed(2)); - const minV = Math.min(...filtered.map(p => p.value)); - const maxV = Math.max(...filtered.map(p => p.value)); - console.warn(`[OBV-DATA] ${plotDef.id} | last5=${last5.join(',')} | min=${minV.toFixed(2)} max=${maxV.toFixed(2)} | totalPts=${filtered.length}`); - } } seriesList.push(series); @@ -936,6 +966,10 @@ export class ChartManager { ); } + // OBV: 최근 50봉 값 기준으로 y축 고정 → 과거 큰 값에 의한 스케일 압축 방지 + if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) { + this._applyObvTightScale(seriesList, result.plots as Record); + } // hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지) const hlineRefs: IPriceLine[] = []; @@ -1052,6 +1086,13 @@ export class ChartManager { if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue; await this._ensureDualLinePlotSeriesForEntry(entry); if (entry.config) this.applyIndicatorStyle(entry.config); + // OBV: tight scale 재적용 (스타일 갱신 후 autoscaleInfoProvider 덮어써질 수 있음) + if (entry.type === 'OBV' && entry.seriesList.length > 0 && this.rawBars.length > 0) { + try { + const result = await calculateIndicator('OBV', this.rawBars.slice(), entry.config?.params ?? {}); + this._applyObvTightScale(entry.seriesList, result.plots as Record); + } catch { /* ok */ } + } } }