/** * 보조지표 설정 화면 — 플롯 행·전역 파라미터 배치 (일목/SMA 단일 화면 컨셉) */ import type { PlotDef } from './indicatorRegistry'; /** 플롯 행에 붙지 않고 상단에만 표시할 키 (우선 순서) */ const GLOBAL_FIRST_KEYS: string[] = [ 'src', 'maType', 'mult', 'multiplier', 'percent', 'bbMult', 'offset', 'sigma', ]; const GLOBAL_FIRST_SET = new Set(GLOBAL_FIRST_KEYS); /** 지표별 플롯 ID → 파라미터 키 (빈 배열 = 자동/공유) */ export const INDICATOR_PLOT_PARAMS: Record> = { MACross: { plot0: ['fastLength'], plot1: ['slowLength'] }, Stochastic: { plot0: ['kLength', 'smooth'], plot1: ['dSmoothing'] }, StochRSI: { plot0: ['lengthRSI', 'lengthStoch', 'smoothK'], plot1: ['smoothD'] }, CCI: { plot0: ['length'], plot1: ['maLength'] }, RSI: { plot0: ['length'], plot1: ['maLength'] }, /** 업비트: OBV 행=이동평균 종류, 신호선 행=기간 (색상 컨트롤 좌측) */ OBV: { plot0: ['maType'], plot1: ['maLength'] }, /** 업비트: 길이·곱·오프셋은 상단 전역 행, 플롯 행은 스타일만 */ BollingerBands: { plot0: [], plot1: [], plot2: [] }, /** 업비트: DI 길이·ADX 스무딩은 상단 전역, 플롯 행은 스타일만 */ DMI: { plot0: [], plot1: [], plot2: [], plot3: [], plot4: [] }, KeltnerChannels: { plot0: ['length', 'multiplier'], plot1: [], plot2: [] }, DonchianChannels: { plot0: ['length'], plot1: [], plot2: [] }, Envelope: { plot0: ['length', 'percent'], plot1: [], plot2: [] }, RVI: { plot0: ['length'], plot1: [] }, FisherTransform: { plot0: ['length'], plot1: [] }, MACD: { plot0: ['fastLength', 'slowLength', 'signalLength'], plot1: [], plot2: [] }, TSI: { plot0: ['longLength', 'shortLength'], plot1: ['signalLength'] }, TRIX: { plot0: ['length'], plot1: ['signalLength'] }, SMIErgodic: { plot0: ['shortLength', 'longLength'], plot1: ['signalLength'] }, SMIErgodicOscillator: { plot0: ['shortLength', 'longLength'], plot1: ['signalLength'] }, UltimateOscillator: { plot0: ['length1', 'length2', 'length3'] }, ConnorsRSI: { plot0: ['rsiLength', 'upDownLength', 'rocLength'] }, Psychological: { plot0: ['length'] }, NewPsychological: { plot0: ['length'] }, InvestPsychological: { plot0: ['length'] }, Disparity: { plot0: ['ultraLength'], plot1: ['shortLength'], plot2: ['midLength'], plot3: ['longLength'], }, IchimokuCloud: { plot0: ['conversionPeriods'], plot1: ['basePeriods'], /** 후행스팬 — 과거 이동 기간 */ plot2: ['chikouDisplacement'], /** 선행스팬1 — 미래 이동 기간 */ plot3: ['senkouDisplacement'], /** 선행스팬2 — Donchian 기간 */ plot4: ['laggingSpan2Periods'], }, }; const PLOT_INDEX_FALLBACK: Record = { 0: [ 'length', 'len', 'kLength', 'fastLength', 'length1', 'lengthRSI', 'rsiLength', 'shortLength', 'ultraLength', 'conversionPeriods', 'turbolen', ], 1: ['dSmoothing', 'slowLength', 'length2', 'smoothD', 'maLength', 'signalLength', 'midLength', 'basePeriods'], 2: ['smooth', 'length3', 'longLength', 'laggingSpan2Periods'], 3: ['longLength', 'displacement'], 4: ['displacement'], }; function keysInParams(keys: string[], params: Record): string[] { return keys.filter(k => k in params); } /** 플롯 한 줄에 표시할 숫자·선택 파라미터 키 */ export function getPlotParamKeys( indicatorType: string, plotId: string, plotIndex: number, plots: PlotDef[], params: Record, ): string[] { const mapped = INDICATOR_PLOT_PARAMS[indicatorType]?.[plotId]; if (mapped !== undefined) { return keysInParams(mapped, params); } if (plots.length === 1) { return Object.keys(params).filter( k => !GLOBAL_FIRST_SET.has(k) && typeof params[k] === 'number', ); } const fallback = keysInParams(PLOT_INDEX_FALLBACK[plotIndex] ?? [], params); if (fallback.length) return fallback; return []; } /** 업비트 BB 인풋 탭 순서 */ const BOLLINGER_GLOBAL_KEYS = ['length', 'mult', 'offset'] as const; /** 플롯에 할당되지 않은 전역 파라미터 (상단 행) */ export function getGlobalParamKeys( indicatorType: string, params: Record, plots: PlotDef[], ): string[] { if (indicatorType === 'BollingerBands') { return BOLLINGER_GLOBAL_KEYS.filter(k => k in params); } const assigned = new Set(); for (let i = 0; i < plots.length; i++) { for (const k of getPlotParamKeys(indicatorType, plots[i].id, i, plots, params)) { assigned.add(k); } } const remaining = Object.keys(params).filter( k => !assigned.has(k) && k !== 'symbolMode' && k !== 'refSymbol' && k !== 'src' && k !== 'maType', ); const ordered = GLOBAL_FIRST_KEYS.filter(k => remaining.includes(k)); const rest = remaining.filter(k => !ordered.includes(k)); return [...ordered, ...rest]; } /** 플롯 행 입력란에 표시할 내용 없음 */ export function plotRowHasNoInputs( indicatorType: string, plotId: string, plotIndex: number, plots: PlotDef[], params: Record, ): boolean { return getPlotParamKeys(indicatorType, plotId, plotIndex, plots, params).length === 0; } /** 파라미터가 상단 전역 행에만 있음 (OBV: maType·maLength) — 플롯 행 '자동' 미표시 */ export function plotParamsOnGlobalRowOnly(indicatorType: string, plotId: string): boolean { const mapped = INDICATOR_PLOT_PARAMS[indicatorType]?.[plotId]; return mapped !== undefined && mapped.length === 0; }