/** * 지표 추가 팝업 Main 탭 — 설정 초기화·차트 인스턴스 병합 */ import type { IndicatorConfig } from '../types'; import type { PlotDef, HLineDef } from './indicatorRegistry'; import { getIndicatorDef, enrichIndicatorConfig, mergePlotDefs } from './indicatorRegistry'; import { MAIN_INDICATOR_TYPES } from './indicatorMainTab'; import { getSettingsIndicatorTypes } from './indicatorSettingsList'; import { initializeIndicatorConfigForEditor } from './indicatorSettingsEditor'; import { normalizeSmaConfig, createDefaultSmaPlotVisibility, } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; import type { IchimokuCloudColors } from './ichimokuConfig'; import { isMainIndicatorType } from './indicatorMainTab'; import { indicatorDefaultsFingerprint } from './indicatorDefaultsFingerprint'; import { isStrategyChartIndicatorId } from './strategyToChartIndicators'; function overlayWorkspaceVisualOntoStrategyIndicator( ind: IndicatorConfig, workspaceByType: Map, ): IndicatorConfig { if (!isStrategyChartIndicatorId(ind.id)) return ind; const ws = workspaceByType.get(ind.type); if (!ws) return ind; /** OBV — slot0 워크스페이스 인스턴스와 동일 params·plots (maLength·실선 신호선 등) */ if (ind.type === 'OBV') { return enrichIndicatorConfig({ ...ws, id: ind.id, hidden: ind.hidden, mergedWith: ind.mergedWith, plotVisibility: { plot0: true, plot1: true, ...ws.plotVisibility }, }); } const def = getIndicatorDef(ind.type); return { ...ind, plots: ws.plots?.length ? mergePlotDefs(ws.plots, ind.plots ?? def?.plots ?? []) : ind.plots, hlines: ws.hlines?.length ? ws.hlines : ind.hlines, cloudColors: ws.cloudColors ?? ind.cloudColors, bandBackground: ws.bandBackground ?? ind.bandBackground, }; } export type MainIndicatorDraftMap = Record; /** 지표 추가 팝업에서 차트 미적용 지표 설정용 modal id 접두사 */ export const INDICATOR_SETTINGS_TEMPLATE_PREFIX = 'template:'; export function indicatorSettingsTemplateId(type: string): string { return `${INDICATOR_SETTINGS_TEMPLATE_PREFIX}${type}`; } export function isIndicatorSettingsTemplateId(id: string): boolean { return id.startsWith(INDICATOR_SETTINGS_TEMPLATE_PREFIX); } export function indicatorTypeFromSettingsId(id: string): string | null { if (!isIndicatorSettingsTemplateId(id)) return null; return id.slice(INDICATOR_SETTINGS_TEMPLATE_PREFIX.length); } export function buildMainIndicatorConfig( type: string, activeIndicators: IndicatorConfig[], getParams: (type: string, defaults: Record) => Record, getVisualConfig: ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors }, ): IndicatorConfig | null { const def = getIndicatorDef(type); if (!def) return null; const active = activeIndicators.find(i => i.type === type); const base: IndicatorConfig = active ?? { id: `template_${type}`, type, params: {}, plots: [], hlines: [], }; return initializeIndicatorConfigForEditor(base, getParams, getVisualConfig); } /** Main 탭 16종 전체 설정 맵 (차트에 없어도 DB 기본값 편집용) */ export function buildMainIndicatorDraftMap( activeIndicators: IndicatorConfig[], getParams: (type: string, defaults: Record) => Record, getVisualConfig: ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors }, ): MainIndicatorDraftMap { const map: MainIndicatorDraftMap = {}; for (const type of MAIN_INDICATOR_TYPES) { const cfg = buildMainIndicatorConfig(type, activeIndicators, getParams, getVisualConfig); if (cfg) map[type] = cfg; } return map; } export function newMainIndicatorId(): string { return `ind_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } function finalizeMainIndicatorConfig(cfg: IndicatorConfig, isNew: boolean): IndicatorConfig { let out = enrichIndicatorConfig(cfg); if (out.type === 'SMA') { out = normalizeSmaConfig({ ...out, plotVisibility: isNew ? (out.plotVisibility ?? createDefaultSmaPlotVisibility()) : out.plotVisibility, }); } else if (out.type === 'IchimokuCloud') { out = normalizeIchimokuConfig(out); } return out; } /** DB에 저장된 전역 기본값 → 차트 인스턴스에 반영 (id·hidden·pane 병합 상태 유지) */ export function mergeGlobalDefaultsOntoChartIndicators( activeIndicators: IndicatorConfig[], getParams: (type: string, defaults: Record) => Record, getVisualConfig: ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors }, workspaceIndicators?: IndicatorConfig[], ): IndicatorConfig[] { const workspaceByType = workspaceIndicators?.length ? new Map(workspaceIndicators.map(i => [i.type, i])) : null; return activeIndicators.map(ind => { /** * strat_* 지표(알림 미니 차트 등) — raw.plots 가 registry 기본값(점선·주황)으로 * 고정되면 getVisualConfig(DB 전역 설정)를 덮어씀. 워크스페이스 ind_* 만 인스턴스 plots 유지. */ const baseInd = isStrategyChartIndicatorId(ind.id) ? { ...ind, plots: [], hlines: [], cloudColors: undefined, bandBackground: undefined, } : ind; const cfg = buildMainIndicatorConfig(ind.type, [baseInd], getParams, getVisualConfig); if (!cfg) return enrichIndicatorConfig(ind); let out = finalizeMainIndicatorConfig( { ...cfg, id: ind.id, hidden: ind.hidden, mergedWith: ind.mergedWith, }, false, ); if (workspaceByType) { out = overlayWorkspaceVisualOntoStrategyIndicator(out, workspaceByType); out = finalizeMainIndicatorConfig(out, false); } return out; }); } /** 전역 기본값 병합 후 실제 렌더 설정이 바뀌었는지 */ export function chartIndicatorsNeedDefaultsRefresh( before: IndicatorConfig[], after: IndicatorConfig[], ): boolean { if (before.length !== after.length) return true; return before.some((ind, i) => indicatorDefaultsFingerprint(ind) !== indicatorDefaultsFingerprint(after[i]), ); } /** Main 탭 설정 → 현재 차트에 올라간 동일 type 인스턴스에 반영 (id·hidden 유지) */ export function mergeMainDraftOntoChart( activeIndicators: IndicatorConfig[], draftByType: MainIndicatorDraftMap, ): IndicatorConfig[] { return activeIndicators.map(ind => { const tpl = draftByType[ind.type]; if (!tpl) return enrichIndicatorConfig(ind); return finalizeMainIndicatorConfig( { ...tpl, id: ind.id, hidden: ind.hidden }, false, ); }); } /** * Main 탭 일괄/설정 적용 — enabledTypes 에 포함된 지표만 차트에 남기고, * 새로 켠 type 은 draft 설정으로 추가, 꺼진 type 은 제거. * Main 탭이 아닌 지표는 그대로 유지. */ export function applyMainDraftToChart( activeIndicators: IndicatorConfig[], draftByType: MainIndicatorDraftMap, enabledTypes: ReadonlySet, ): IndicatorConfig[] { const result: IndicatorConfig[] = activeIndicators .filter(ind => !isMainIndicatorType(ind.type)) .map(ind => enrichIndicatorConfig(ind)); const seen = new Set(); for (const ind of activeIndicators) { if (!isMainIndicatorType(ind.type)) continue; if (!enabledTypes.has(ind.type)) continue; seen.add(ind.type); const tpl = draftByType[ind.type]; if (!tpl) { result.push(enrichIndicatorConfig(ind)); continue; } result.push(finalizeMainIndicatorConfig( { ...tpl, id: ind.id, hidden: ind.hidden }, false, )); } for (const type of MAIN_INDICATOR_TYPES) { if (!enabledTypes.has(type) || seen.has(type)) continue; const tpl = draftByType[type]; if (!tpl) continue; result.push(finalizeMainIndicatorConfig( { ...tpl, id: newMainIndicatorId() }, true, )); } return result; } /** 차트에 올라간 Main 탭 type 집합 */ export function mainTypesOnChart(indicators: IndicatorConfig[]): Set { return new Set( indicators.filter(i => isMainIndicatorType(i.type)).map(i => i.type), ); } export function mainDraftListFromMap(map: MainIndicatorDraftMap): IndicatorConfig[] { return MAIN_INDICATOR_TYPES .map(type => map[type]) .filter((c): c is IndicatorConfig => c != null); } /** 설정 UI 전체 지표 draft 맵 */ export function buildAllIndicatorDraftMap( activeIndicators: IndicatorConfig[], getParams: (type: string, defaults: Record) => Record, getVisualConfig: ( type: string, defaultPlots?: PlotDef[], defaultHlines?: HLineDef[], ) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors }, ): MainIndicatorDraftMap { const map: MainIndicatorDraftMap = {}; for (const type of getSettingsIndicatorTypes()) { const cfg = buildMainIndicatorConfig(type, activeIndicators, getParams, getVisualConfig); if (cfg) map[type] = cfg; } return map; } /** draft 맵 → DB 저장용 config 배열 (순서 유지) */ export function allDraftListFromMap(map: MainIndicatorDraftMap): IndicatorConfig[] { return getSettingsIndicatorTypes() .map(type => map[type]) .filter((c): c is IndicatorConfig => c != null); } /** 차트에 올라간 보조지표 type 집합 (Main 외 포함) */ export function indicatorTypesOnChart(indicators: IndicatorConfig[]): Set { return new Set(indicators.map(i => i.type)); } const MANAGED_TYPES = getSettingsIndicatorTypes(); const MANAGED_SET = new Set(MANAGED_TYPES); /** * 설정 UI에서 관리하는 모든 type — enabledTypes 기준으로 차트 병합. * 관리 대상이 아닌 type 은 변경 없이 유지. */ export function applyAllDraftToChart( activeIndicators: IndicatorConfig[], draftByType: MainIndicatorDraftMap, enabledTypes: ReadonlySet, ): IndicatorConfig[] { const result: IndicatorConfig[] = activeIndicators .filter(ind => !MANAGED_SET.has(ind.type)) .map(ind => enrichIndicatorConfig(ind)); const seen = new Set(); for (const ind of activeIndicators) { if (!MANAGED_SET.has(ind.type)) continue; if (!enabledTypes.has(ind.type)) continue; seen.add(ind.type); const tpl = draftByType[ind.type]; if (!tpl) { result.push(enrichIndicatorConfig(ind)); continue; } result.push(finalizeMainIndicatorConfig( { ...tpl, id: ind.id, hidden: ind.hidden }, false, )); } for (const type of MANAGED_TYPES) { if (!enabledTypes.has(type) || seen.has(type)) continue; const tpl = draftByType[type]; if (!tpl) continue; result.push(finalizeMainIndicatorConfig( { ...tpl, id: newMainIndicatorId() }, true, )); } return result; }