186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
/**
|
|
* 단순이동평균(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<string, number | string | boolean> | 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<string, number | string | boolean> {
|
|
const params: Record<string, number | string | boolean> = { 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<string, boolean> {
|
|
const vis: Record<string, boolean> = {};
|
|
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<string, number | string | boolean> = {
|
|
...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<string, boolean> = {
|
|
...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<string, number | string | boolean>
|
|
): 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;
|
|
}
|