/** * 단순이동평균(SMA) — MA1~MA11 다중선 설정 및 계산 */ import type { OHLCVBar } from '../types'; import type { IndicatorConfig } from '../types'; import type { PlotDef } from './indicatorRegistry'; import type { IndicatorResult } from './indicatorRegistry'; export const SMA_MA_COUNT = 11; /** 소스 기본값 — 종가 */ export const SMA_DEFAULT_SRC = 'close' as const; export const SMA_SRC_OPTIONS = [ 'close', 'open', 'high', 'low', 'hl2', 'hlc3', 'ohlc4', ] as const; export type SmaSource = (typeof SMA_SRC_OPTIONS)[number]; /** MA1~MA11 기본 기간 */ export const SMA_DEFAULT_PERIODS = [5, 12, 16, 20, 26, 34, 50, 60, 72, 90, 120] as const; /** MA1~MA11 기본 색상 */ export const SMA_DEFAULT_COLORS = [ '#2962FF', '#FF6D00', '#4CAF50', '#E91E63', '#9C27B0', '#00BCD4', '#FFC107', '#795548', '#607D8B', '#3F51B5', '#8BC34A', ] as const; /** MA1~MA5 기본 ON, MA6~11 OFF */ export const SMA_DEFAULT_ENABLED = [ true, true, true, true, true, false, false, false, false, false, false, ] as const; export function smaPlotId(index: number): string { return `plot${index}`; } export function smaPeriodKey(maIndex: number): string { return `period${maIndex}`; } export function createDefaultSmaPlots(): PlotDef[] { return SMA_DEFAULT_PERIODS.map((_, i) => ({ id: smaPlotId(i), title: `MA${i + 1}`, color: SMA_DEFAULT_COLORS[i], type: 'line', lineWidth: 2, lineStyle: 'solid', })); } /** 저장·레거시 파라미터에서 유효한 SMA 소스 키 반환 (기본: 종가) */ export function resolveSmaSource( params: Record | undefined, ): SmaSource { const raw = params?.src ?? params?.source; const s = typeof raw === 'string' ? raw.trim().toLowerCase() : ''; if ((SMA_SRC_OPTIONS as readonly string[]).includes(s)) { return s as SmaSource; } return SMA_DEFAULT_SRC; } export function createDefaultSmaParams(): Record { const params: Record = { src: SMA_DEFAULT_SRC }; for (let i = 1; i <= SMA_MA_COUNT; i++) { params[smaPeriodKey(i)] = SMA_DEFAULT_PERIODS[i - 1]; } return params; } export function createDefaultSmaPlotVisibility(): Record { const vis: Record = {}; for (let i = 0; i < SMA_MA_COUNT; i++) { vis[smaPlotId(i)] = SMA_DEFAULT_ENABLED[i]; } return vis; } /** * 레거시 단일 len 파라미터·plot0 단일 플롯 → MA1~11 구성으로 정규화 */ export function normalizeSmaConfig(config: IndicatorConfig): IndicatorConfig { if (config.type !== 'SMA') return config; const params: Record = { ...createDefaultSmaParams(), ...config.params, }; // 구버전 len → period1 const legacyLen = params.len; if (legacyLen != null && typeof legacyLen === 'number') { if (params.period1 == null || params.period1 === SMA_DEFAULT_PERIODS[0]) { params.period1 = legacyLen; } } delete params.len; delete params.source; params.src = resolveSmaSource(params); for (let i = 1; i <= SMA_MA_COUNT; i++) { const key = smaPeriodKey(i); const v = params[key]; if (typeof v !== 'number' || v < 1) { params[key] = SMA_DEFAULT_PERIODS[i - 1]; } } const defaultPlots = createDefaultSmaPlots(); const savedById = new Map((config.plots ?? []).map(p => [p.id, p])); const plots: PlotDef[] = defaultPlots.map((def, i) => { const saved = savedById.get(def.id) ?? config.plots?.[i]; return saved ? { ...def, ...saved, id: def.id, title: `MA${i + 1}`, type: 'line' as const } : def; }); const plotVisibility: Record = { ...createDefaultSmaPlotVisibility(), ...config.plotVisibility, }; for (let i = 0; i < SMA_MA_COUNT; i++) { const id = smaPlotId(i); if (plotVisibility[id] === undefined) { plotVisibility[id] = SMA_DEFAULT_ENABLED[i]; } } return { ...config, params, plots, plotVisibility }; } function sourceValues(bars: OHLCVBar[], src: string): number[] { switch (src) { case 'open': return bars.map(b => b.open); case 'high': return bars.map(b => b.high); case 'low': return bars.map(b => b.low); case 'hl2': return bars.map(b => (b.high + b.low) / 2); case 'hlc3': return bars.map(b => (b.high + b.low + b.close) / 3); case 'ohlc4': return bars.map(b => (b.open + b.high + b.low + b.close) / 4); default: return bars.map(b => b.close); } } function computeSma(values: number[], period: number): (number | null)[] { const p = Math.max(1, Math.floor(period)); const out: (number | null)[] = []; for (let i = 0; i < values.length; i++) { if (i < p - 1) { out.push(null); continue; } let sum = 0; for (let j = i - p + 1; j <= i; j++) sum += values[j]; out.push(sum / p); } return out; } /** MA1~MA11 각각 plot0~plot10 데이터 계산 */ export function calculateSmaMulti( bars: OHLCVBar[], params: Record ): IndicatorResult { const src = resolveSmaSource(params); const values = sourceValues(bars, src); const plots: IndicatorResult = {}; for (let i = 0; i < SMA_MA_COUNT; i++) { const periodKey = smaPeriodKey(i + 1); const period = Number(params[periodKey] ?? SMA_DEFAULT_PERIODS[i]); const sma = computeSma(values, period); const plotId = smaPlotId(i); plots[plotId] = []; for (let j = 0; j < bars.length; j++) { const v = sma[j]; if (v == null || Number.isNaN(v)) continue; plots[plotId].push({ time: bars[j].time, value: v }); } } return plots; }