/** * 보조지표 설정 — 숫자 파라미터 허용 범위·드롭다운 목록 */ export interface NumericParamSpec { min: number; max: number; step: number; allowNegative: boolean; decimal: boolean; /** 드롭다운에 표시할 허용 값 목록 */ options: number[]; } const COMMON_PERIODS = [ 1, 2, 3, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 20, 21, 24, 26, 28, 34, 50, 52, 60, 72, 90, 100, 120, 200, ]; const DECIMAL_MULT = [0.1, 0.2, 0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5]; function roundTo(v: number, decimals: number): number { const f = 10 ** decimals; return Math.round(v * f) / f; } function mergeOptions( min: number, max: number, step: number, decimal: boolean, presets: number[], maxCount = 56, include?: number, ): number[] { const set = new Set(); const dec = decimal ? (step < 1 ? 2 : 1) : 0; for (const p of presets) { if (p >= min && p <= max) set.add(roundTo(p, dec)); } if (include != null && !isNaN(include) && include >= min && include <= max) { set.add(roundTo(include, dec)); } for (let v = min; v <= max + step * 0.001 && set.size < maxCount; v += step) { set.add(roundTo(v, dec)); } return Array.from(set).sort((a, b) => a - b); } function isPeriodKey(key: string): boolean { return /^(length|len|period\d*|kLength|dSmoothing|smooth|adxSmoothing|diLength|ccilength|turbolen|signalLength|fastLength|slowLength|longLength|shortLength|maLength|ultraLength|midLength|longLength|displacement|chikouDisplacement|senkouDisplacement|conversionPeriods|basePeriods|laggingSpan2Periods|length1|length2|length3|lengthRSI|lengthStoch|rsiLength|upDownLength|rocLength|stdevLength|atrLength|p|q|x)$/i.test(key); } function isMultKey(key: string): boolean { return /^(mult|multiplier|percent|bbMult)$/i.test(key); } function isOffsetKey(key: string): boolean { return /^offset$/i.test(key); } function isSigmaKey(key: string): boolean { return /^sigma$/i.test(key); } /** 지표 파라미터 키별 숫자 스펙 */ export function getNumericParamSpec( indicatorType: string, paramKey: string, currentValue?: number, ): NumericParamSpec { if (isPeriodKey(paramKey)) { const max = paramKey === 'rocLength' ? 200 : 500; return { min: 1, max, step: 1, allowNegative: false, decimal: false, options: mergeOptions(1, max, 1, false, COMMON_PERIODS, 56, currentValue), }; } if (isMultKey(paramKey)) { const isPercent = paramKey === 'percent'; return { min: isPercent ? 0.01 : 0.1, max: isPercent ? 1 : 10, step: isPercent ? 0.01 : 0.1, allowNegative: false, decimal: true, options: mergeOptions( isPercent ? 0.01 : 0.1, isPercent ? 1 : 10, isPercent ? 0.01 : 0.1, true, isPercent ? [0.05, 0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1] : DECIMAL_MULT, 40, currentValue, ), }; } if (isOffsetKey(paramKey)) { if (indicatorType === 'ALMA') { return { min: 0, max: 1, step: 0.05, allowNegative: false, decimal: true, options: mergeOptions(0, 1, 0.05, true, [0, 0.5, 0.85, 1], 24, currentValue), }; } return { min: -500, max: 500, step: 1, allowNegative: true, decimal: false, options: mergeOptions(-500, 500, 1, false, [-26, -9, 0, 9, 26], 48, currentValue), }; } if (isSigmaKey(paramKey)) { return { min: 1, max: 30, step: 1, allowNegative: false, decimal: false, options: mergeOptions(1, 30, 1, false, [4, 6, 8, 10, 14, 20], 30, currentValue), }; } // SMI 등 소수 오실레이터 임계 if (indicatorType === 'SMIErgodic' || indicatorType === 'SMIErgodicOscillator') { return { min: 1, max: 100, step: 1, allowNegative: false, decimal: false, options: mergeOptions(1, 100, 1, false, [5, 10, 20, 40], 40, currentValue), }; } // 기본: 정수 1~100 return { min: 1, max: 500, step: 1, allowNegative: false, decimal: false, options: mergeOptions(1, 500, 1, false, COMMON_PERIODS, 56, currentValue), }; } /** 수평선(과열/침체) 기준값 */ export function getHlinePriceSpec(indicatorType: string, current: number): NumericParamSpec { const byType: Record = { RSI: [20, 30, 50, 70, 80], Stochastic: [20, 50, 80], StochRSI: [20, 50, 80], CCI: [-200, -100, 0, 100, 200], WilliamsPercentRange: [-80, -50, -20], MACD: [-50, 0, 50], ADX: [20, 25, 40], DMI: [10, 20, 30], TRIX: [-0.05, -0.01, 0, 0.01, 0.05], MOM: [-50, 0, 50], ROC: [-20, 0, 20], CMO: [-50, 0, 50], DPO: [-100, 0, 100], FisherTransform: [-2.5, 0, 2.5], SMIErgodic: [-0.4, 0, 0.4], SMIErgodicOscillator: [-0.4, 0, 0.4], Disparity: [90, 95, 100, 105, 110], VR: [50, 100, 150, 200, 250], Psychological: [25, 50, 75], NewPsychological: [-50, 0, 50], InvestPsychological: [25, 50, 75], }; const presets = byType[indicatorType] ?? [-200, -100, 0, 100, 200]; const allowNegative = presets.some(p => p < 0) || indicatorType !== 'RSI'; const min = Math.min(-500, ...presets, current) - 50; const max = Math.max(500, ...presets, current) + 50; return { min, max, step: 1, allowNegative: true, decimal: true, options: mergeOptions(min, max, allowNegative ? 10 : 5, true, presets, 48, current), }; } export function getLineWidthSpec(min = 1, max = 5): NumericParamSpec { const options = Array.from({ length: max - min + 1 }, (_, i) => min + i); return { min, max, step: 1, allowNegative: false, decimal: false, options, }; } export function getOpacityPercentSpec(current?: number): NumericParamSpec { const options = Array.from({ length: 21 }, (_, i) => i * 5); return { min: 0, max: 100, step: 1, allowNegative: false, decimal: false, options: mergeOptions(0, 100, 5, false, options, 21, current), }; }