diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 40a0cba..ece4eb2 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -181,6 +181,13 @@ function seriesByPlotId( return i >= 0 ? entry.seriesList[i] : undefined; } +/** hline·음영 프리미티브 부착 대상 — histogram+line 지표(MACD 등)는 line 시리즈에 부착 */ +function hlineAnchorSeries(entry: Pick): ISeriesApi | undefined { + if (entry.seriesList.length === 0) return undefined; + const lineIdx = entry.seriesMeta.findIndex(m => !m.isHistogram); + return entry.seriesList[lineIdx >= 0 ? lineIdx : 0]; +} + function shouldShowBbBandFill(config: IndicatorConfig): boolean { const bg = resolveBbBandBackground(config.bandBackground); if (!bg.visible) return false; @@ -1052,7 +1059,7 @@ export class ChartManager { const indicatorHidden = !this._isIndicatorEffectivelyVisible(config); for (const pl of entry.hlineRefs) { - try { entry.seriesList[0]?.removePriceLine(pl); } catch { /* ok */ } + try { hlineAnchorSeries(entry)?.removePriceLine(pl); } catch { /* ok */ } } entry.hlineRefs = []; for (const s of entry.seriesList) { @@ -1228,7 +1235,7 @@ export class ChartManager { let fillPrimitive: IndicatorFillPrimitive | undefined; if (!indicatorHidden && seriesList.length > 0) { - const firstSeries = seriesList[0]; + const hlineHost = hlineAnchorSeries({ seriesList, seriesMeta }); const hlines = config.hlines ?? def?.hlines ?? []; // HLineStyle → lightweight-charts LineStyle 변환 @@ -1238,28 +1245,30 @@ export class ChartManager { return LineStyle.Dashed; // 기본값 }; - for (const hl of hlines) { - if (hl.visible === false) continue; - const pl = firstSeries.createPriceLine({ - price: hl.price, - color: hl.color, - lineWidth: Math.min(4, hl.lineWidth ?? 1) as 1|2|3|4, - lineStyle: toLineStyle(hl.lineStyle), - axisLabelVisible: false, - }); - hlineRefs.push(pl); - } + if (hlineHost) { + for (const hl of hlines) { + if (hl.visible === false) continue; + const pl = hlineHost.createPriceLine({ + price: hl.price, + color: hl.color, + lineWidth: Math.min(4, hl.lineWidth ?? 1) as 1|2|3|4, + lineStyle: toLineStyle(hl.lineStyle), + axisLabelVisible: false, + }); + hlineRefs.push(pl); + } - // 과열선/침체선 레이블이 있는 hlines 가 있으면 음영 프리미티브 부착 - const prices = hlines.filter(h => h.visible !== false).map(h => h.price); - const hasZone = hlines.some(h => { - const lbl = h.label ?? getHLineLabel(h.price, prices); - return (lbl === '과열선' || lbl === '침체선') && h.visible !== false; - }); - const hasBg = config.hlinesBackground?.visible !== false; - if (hasZone || hasBg) { - fillPrimitive = new IndicatorFillPrimitive(hlines, config.hlinesBackground); - firstSeries.attachPrimitive(fillPrimitive); + // 과열선/침체선 레이블이 있는 hlines 가 있으면 음영 프리미티브 부착 + const prices = hlines.filter(h => h.visible !== false).map(h => h.price); + const hasZone = hlines.some(h => { + const lbl = h.label ?? getHLineLabel(h.price, prices); + return (lbl === '과열선' || lbl === '침체선') && h.visible !== false; + }); + const hasBg = config.hlinesBackground?.visible !== false; + if (hasZone || hasBg) { + fillPrimitive = new IndicatorFillPrimitive(hlines, config.hlinesBackground); + hlineHost.attachPrimitive(fillPrimitive); + } } } @@ -1482,11 +1491,12 @@ export class ChartManager { const entry = this.indicators.get(id); if (!entry) return false; if (entry.seriesList.length > 0) { + const hlineHost = hlineAnchorSeries(entry); for (const pl of entry.hlineRefs) { - try { entry.seriesList[0].removePriceLine(pl); } catch { /* ok */ } + try { hlineHost?.removePriceLine(pl); } catch { /* ok */ } } if (entry.fillPrimitive) { - try { entry.seriesList[0].detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } + try { hlineHost?.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } } } if (entry.cloudPlugin) { @@ -1518,8 +1528,8 @@ export class ChartManager { try { this.mainSeries?.detachPrimitive(entry.cloudPlugin); } catch { /* ok */ } for (const s of entry.seriesList) { try { s.detachPrimitive(entry.cloudPlugin); } catch { /* ok */ } } } - if (entry.fillPrimitive && entry.seriesList[0]) { - try { entry.seriesList[0].detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } + if (entry.fillPrimitive) { + try { hlineAnchorSeries(entry)?.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } } detachBbBandFill(entry); for (const s of entry.seriesList) { try { this.chart.removeSeries(s); } catch { /* ok */ } } @@ -3524,7 +3534,7 @@ export class ChartManager { // ── hlines(과열선/중앙선/침체선) 동기화 ───────────────────────────────── if (entry.seriesList.length > 0) { - const firstSeries = entry.seriesList[0]; + const hlineHost = hlineAnchorSeries(entry); const hlines = config.hlines ?? def?.hlines ?? []; const toLineStyle2 = (s?: string): number => { @@ -3535,19 +3545,19 @@ export class ChartManager { // 기존 price line 제거 for (const pl of entry.hlineRefs) { - try { firstSeries.removePriceLine(pl); } catch { /* 이미 제거됨 */ } + try { hlineHost?.removePriceLine(pl); } catch { /* 이미 제거됨 */ } } entry.hlineRefs = []; if (entry.fillPrimitive) { - try { firstSeries.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } + try { hlineHost?.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } entry.fillPrimitive = undefined; } - if (indicatorVisible) { + if (indicatorVisible && hlineHost) { for (const hl of hlines) { if (hl.visible === false) continue; - const pl = firstSeries.createPriceLine({ + const pl = hlineHost.createPriceLine({ price: hl.price, color: hl.color, lineWidth: Math.min(4, hl.lineWidth ?? 1) as 1|2|3|4, @@ -3565,7 +3575,7 @@ export class ChartManager { const hasBg = config.hlinesBackground?.visible !== false; if (hasZone || hasBg) { entry.fillPrimitive = new IndicatorFillPrimitive(hlines, config.hlinesBackground); - firstSeries.attachPrimitive(entry.fillPrimitive); + hlineHost.attachPrimitive(entry.fillPrimitive); } } } @@ -3873,14 +3883,14 @@ export class ChartManager { } as Parameters['applyOptions']>[0]); } catch { /* ok */ } } - const first = entry.seriesList[0]; - if (first) { + const hlineHost = hlineAnchorSeries(entry); + if (hlineHost) { for (const pl of entry.hlineRefs) { - try { first.removePriceLine(pl); } catch { /* ok */ } + try { hlineHost.removePriceLine(pl); } catch { /* ok */ } } entry.hlineRefs = []; if (entry.fillPrimitive) { - try { first.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } + try { hlineHost.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ } } } if (entry.cloudPlugin) { diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index e3976d5..6d27200 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -88,6 +88,10 @@ function resolveMergedHLinePrice( savedPrice: number, defPrice: number, ): number { + // MACD·TRIX 등 registry 기본 중앙선이 0인 지표: 잘못 저장된 종가급 값(69872 등) 교정 + if (roleLabel === '중앙선' && defPrice === 0) { + return 0; + } if (roleLabel === '중앙선' && savedPrice === 0 && defPrice !== 0) { return defPrice; } @@ -205,7 +209,7 @@ export function normalizeHLines( const hasUnder = withFixedLabels.some(h => getLabel(h) === '침체선'); if (hasOver && hasCenter && hasUnder) { - return withFixedLabels.sort((a, b) => b.price - a.price); + return clampZeroCenterHLines(withFixedLabels, defaults).sort((a, b) => b.price - a.price); } const result = [...withFixedLabels]; @@ -245,7 +249,22 @@ export function normalizeHLines( }); } - return result.sort((a, b) => b.price - a.price); + return clampZeroCenterHLines(result, defaults).sort((a, b) => b.price - a.price); +} + +/** registry 기본 중앙선이 0인 지표에서 잘못 저장된 중앙선 가격을 0으로 고정 */ +function clampZeroCenterHLines(hlines: HLineDef[], defaults?: HLineDef[]): HLineDef[] { + const defPrices = (defaults ?? []).map(d => d.price); + const defCenter = (defaults ?? []).find( + d => (d.label ?? getHLineLabel(d.price, defPrices)) === '중앙선', + ); + if (defCenter?.price !== 0) return hlines; + + const prices = hlines.map(h => h.price); + return hlines.map(h => { + const lbl = h.label ?? getHLineLabel(h.price, prices); + return lbl === '중앙선' ? { ...h, price: 0 } : h; + }); } export interface IndicatorDef {