From 52300379c473bc2efd6436db1e7a042b2fc21e39 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 01:29:42 +0900 Subject: [PATCH] =?UTF-8?q?macd=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/utils/ChartManager.ts | 6 +-- frontend/src/utils/indicatorRegistry.ts | 61 +++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index ece4eb2..0166917 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -43,7 +43,7 @@ import { } from './chartTimeFormat'; import { DEFAULT_DISPLAY_TIMEZONE, getTimezoneOffsetSec } from './timezone'; import { getPaneHostId, sortIndicatorsForPaneLoad } from './indicatorPaneMerge'; -import { calculateIndicator, enrichIndicatorConfig, getIndicatorDef, getHLineLabel, type PlotData, type MarkerData } from './indicatorRegistry'; +import { calculateIndicator, enrichIndicatorConfig, getIndicatorDef, getHLineLabel, resolveHLineDrawPrice, type PlotData, type MarkerData } from './indicatorRegistry'; import { filterObvPlotDataForChart } from './customIndicators'; import { IchimokuCloudPlugin, type IchimokuCloudPoint } from './IchimokuCloudPlugin'; @@ -1249,7 +1249,7 @@ export class ChartManager { for (const hl of hlines) { if (hl.visible === false) continue; const pl = hlineHost.createPriceLine({ - price: hl.price, + price: resolveHLineDrawPrice(config.type, hl, hlines), color: hl.color, lineWidth: Math.min(4, hl.lineWidth ?? 1) as 1|2|3|4, lineStyle: toLineStyle(hl.lineStyle), @@ -3558,7 +3558,7 @@ export class ChartManager { for (const hl of hlines) { if (hl.visible === false) continue; const pl = hlineHost.createPriceLine({ - price: hl.price, + price: resolveHLineDrawPrice(config.type, hl, hlines), color: hl.color, lineWidth: Math.min(4, hl.lineWidth ?? 1) as 1|2|3|4, lineStyle: toLineStyle2(hl.lineStyle), diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index 6d27200..89410eb 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -157,6 +157,10 @@ export function mergeHlines( ? savedList[0] : undefined; + const defRoleLabels = new Set( + defs.map(d => d.label ?? getHLineLabel(d.price, defs.map(x => x.price))), + ); + return defs .map(def => { const wantLabel = def.label ?? getHLineLabel(def.price, defs.map(d => d.price)); @@ -172,7 +176,9 @@ export function mergeHlines( .concat( savedList.filter(h => { const lbl = labelOf(h); - return !defs.some(d => (d.label ?? getHLineLabel(d.price, defs.map(x => x.price))) === lbl); + if (defRoleLabels.has(lbl)) return false; + // registry에 없는 역할(잘못 분류된 과열/침체 orphan)은 제거 — 종가가 과열선으로 저장되는 MACD 버그 방지 + return defRoleLabels.size > 1; }).map(h => ({ ...h, label: labelOf(h) })), ) .sort((a, b) => b.price - a.price); @@ -254,19 +260,64 @@ export function normalizeHLines( /** registry 기본 중앙선이 0인 지표에서 잘못 저장된 중앙선 가격을 0으로 고정 */ function clampZeroCenterHLines(hlines: HLineDef[], defaults?: HLineDef[]): HLineDef[] { - const defPrices = (defaults ?? []).map(d => d.price); - const defCenter = (defaults ?? []).find( + const defs = defaults ?? []; + const defPrices = defs.map(d => d.price); + const defCenter = defs.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 defPriceSet = new Set(defPrices); + let list = hlines; + // MACD·AO 등 중앙 0선 단일 def — registry에 없는 가격(종가 orphan) 제거 + if (defs.length === 1 && defs[0].price === 0) { + const orphanWasVisible = hlines.some(h => { + const prices = hlines.map(x => x.price); + const lbl = h.label ?? getHLineLabel(h.price, prices); + return lbl !== '중앙선' && h.visible !== false && !defPriceSet.has(h.price); + }); + list = hlines.filter(h => { + const prices = hlines.map(x => x.price); + const lbl = h.label ?? getHLineLabel(h.price, prices); + if (lbl === '중앙선') return true; + return defPriceSet.has(h.price); + }); + if (orphanWasVisible) { + list = list.map(h => { + const prices = list.map(x => x.price); + const lbl = h.label ?? getHLineLabel(h.price, prices); + return lbl === '중앙선' ? { ...h, visible: true } : h; + }); + } + } + + const prices = list.map(h => h.price); + return list.map(h => { const lbl = h.label ?? getHLineLabel(h.price, prices); return lbl === '중앙선' ? { ...h, price: 0 } : h; }); } +/** 차트 draw 시 hline 실제 가격 — 중앙 0선 지표는 항상 0 */ +export function resolveHLineDrawPrice( + indicatorType: string, + hl: HLineDef, + allHlines: HLineDef[], +): number { + const def = getIndicatorDef(indicatorType); + const defaults = def?.hlines ?? []; + const defPrices = defaults.map(d => d.price); + const prices = allHlines.map(h => h.price); + const lbl = hl.label ?? getHLineLabel(hl.price, prices); + const defCenter = defaults.find( + d => (d.label ?? getHLineLabel(d.price, defPrices)) === '중앙선', + ); + if (lbl === '중앙선' && defCenter?.price === 0) { + return 0; + } + return hl.price; +} + export interface IndicatorDef { type: string; name: string;