diff --git a/frontend/src/hooks/useIndicatorSettings.ts b/frontend/src/hooks/useIndicatorSettings.ts index 5734dc8..5e1f95f 100644 --- a/frontend/src/hooks/useIndicatorSettings.ts +++ b/frontend/src/hooks/useIndicatorSettings.ts @@ -22,7 +22,7 @@ */ import { useEffect, useRef, useCallback, useState } from 'react'; -import { getIndicatorDef, mergePlotDefs, normalizeHLines, PlotDef, HLineDef } from '../utils/indicatorRegistry'; +import { getIndicatorDef, mergePlotDefs, normalizeHLines, normalizeObvParams, PlotDef, HLineDef } from '../utils/indicatorRegistry'; import { createDefaultSmaParams, createDefaultSmaPlots, normalizeSmaConfig } from '../utils/smaConfig'; import { normalizeIchimokuConfig, mergeIchimokuPlots } from '../utils/ichimokuConfig'; import { @@ -177,6 +177,9 @@ export function useIndicatorSettings(sessionKey = 0) { if (type === 'IchimokuCloud') { return normalizeIchimokuConfig({ id: '', type: 'IchimokuCloud', params: merged }).params; } + if (type === 'OBV') { + return normalizeObvParams(merged); + } return merged; }, [settingsRevision], diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index a4e81e5..150e580 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -791,7 +791,7 @@ export class ChartManager { const showSeriesTitle = pane < 2 && showPriceLabel; series = this.chart.addSeries(LineSeries, { color: plotDef.color, - lineWidth: (plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4, + lineWidth: Math.max(1, plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4, lineStyle: plotSeriesLineStyle(plotDef.lineStyle), priceLineVisible: false, lastValueVisible: showPriceLabel, diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index 2a95ee9..35c7a26 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -441,6 +441,45 @@ export function mergePlotDefs(saved: PlotDef[] | undefined, defaults: PlotDef[]) }); } +/** OBV·CCI·RSI·TRIX — registry 신호선(plot1) 병합·표시 보장 (DB에 plot0만 저장된 경우 대비) */ +const SIGNAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']); + +function normalizeSignalLineIndicatorPlots( + type: string, + plots: PlotDef[], + registryPlots: PlotDef[], + plotVisibility: Record, + params: Record, +): { plots: PlotDef[]; plotVisibility: Record } { + if (!SIGNAL_LINE_INDICATOR_TYPES.has(type)) { + return { plots, plotVisibility }; + } + const merged = mergePlotDefs(plots, registryPlots); + const regSignal = registryPlots.find(p => p.id === 'plot1'); + const nextPlots = regSignal + ? merged.map(p => { + if (p.id !== 'plot1') return p; + return { + ...regSignal, + ...p, + id: 'plot1', + title: regSignal.title, + type: 'line' as const, + lineWidth: Math.max(1, p.lineWidth ?? regSignal.lineWidth ?? 1), + }; + }) + : merged; + const nextVis = { ...plotVisibility }; + if (type === 'OBV') { + nextVis.plot1 = true; + } else if ((type === 'CCI' || type === 'RSI') && params.maType === 'None') { + nextVis.plot1 = false; + } else if (nextVis.plot1 !== false) { + nextVis.plot1 = true; + } + return { plots: nextPlots, plotVisibility: nextVis }; +} + /** 업비트 OBV 신호선 MA 종류 (단순·지수·가중만) */ export const OBV_MA_TYPE_OPTIONS = ['SMA', 'EMA', 'WMA'] as const; export type ObvMaType = (typeof OBV_MA_TYPE_OPTIONS)[number]; @@ -520,18 +559,20 @@ export function enrichIndicatorConfig( if (cfgNorm.type === 'DMI') { params = normalizeDmiParams(params); } - const plots = cfgNorm.type === 'BollingerBands' + let plots = cfgNorm.type === 'BollingerBands' ? mergeBbPlots(cfgNorm.plots, def.plots) : mergePlotDefs(cfgNorm.plots, def.plots); - const plotVisibility: Record = { ...cfgNorm.plotVisibility }; + let plotVisibility: Record = { ...cfgNorm.plotVisibility }; for (const p of plots) { if (plotVisibility[p.id] === undefined) plotVisibility[p.id] = true; } - if (cfgNorm.type === 'OBV') { - plotVisibility.plot1 = plotVisibility.plot1 !== false; - } else if ((cfgNorm.type === 'CCI' || cfgNorm.type === 'RSI') && params.maType === 'None') { - plotVisibility.plot1 = false; - } + ({ plots, plotVisibility } = normalizeSignalLineIndicatorPlots( + cfgNorm.type, + plots, + def.plots, + plotVisibility, + params, + )); const out: import('../types').IndicatorConfig = { ...cfgNorm, @@ -636,6 +677,9 @@ export async function calculateIndicator( if (type === 'CCI') { calcParams = normalizeCciParams(params); } + if (type === 'OBV') { + calcParams = normalizeObvParams(params); + } if (type === 'IchimokuCloud') { const { senkou } = resolveIchimokuDisplacements(params); calcParams = { ...params, displacement: senkou }; diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index 3eb0529..6df925d 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -8,6 +8,8 @@ import { enrichIndicatorConfig, getIndicatorDef, getHLineLabel, + mergePlotDefs, + normalizeObvParams, type HLineDef, } from './indicatorRegistry'; import { buildMainIndicatorConfig } from './indicatorMainConfig'; @@ -199,6 +201,17 @@ function buildOneIndicator( plots: mainSma?.plots ?? cfg.plots, plotVisibility: mainSma?.plotVisibility ?? cfg.plotVisibility, }); + } else if (ref.registryType === 'OBV') { + const p = normalizeObvParams({ ...cfg.params }); + const signalLen = ref.rightPeriod ?? ref.period; + if (signalLen != null && signalLen > 0) { + p.maLength = signalLen; + } + cfg = { + ...cfg, + params: p, + plots: mergePlotDefs(cfg.plots, def.plots), + }; } else if (ref.period != null && ref.period > 0) { cfg = { ...cfg,