Files
goldenChart/frontend/src/utils/smaConfig.ts
T

225 lines
6.9 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}`;
}
/** 설정 기간 → 표시명 MA5일 (보조지표·차트·전략 공통) */
export function formatSmaPeriodDisplayLabel(
period: number | string | boolean | undefined,
fallback = SMA_DEFAULT_PERIODS[0],
): string {
const raw = typeof period === 'number' ? period : Number(period);
const p = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
return `MA${p}일`;
}
export function resolveSmaMaPeriod(
params: Record<string, number | string | boolean> | undefined,
maIndex: number,
): number {
const slot = Math.max(1, Math.min(SMA_MA_COUNT, Math.floor(maIndex)));
const raw = params?.[smaPeriodKey(slot)];
if (typeof raw === 'number' && raw > 0) return Math.floor(raw);
return SMA_DEFAULT_PERIODS[slot - 1];
}
/** MA 슬롯(1~11) + params → MA5일 */
export function formatSmaMaDisplayLabel(
params: Record<string, number | string | boolean> | undefined,
maIndex: number,
): string {
return formatSmaPeriodDisplayLabel(resolveSmaMaPeriod(params, maIndex));
}
/** params 변경 시 plot.title 을 MA{period}일 로 동기화 */
export function syncSmaPlotTitles(
params: Record<string, number | string | boolean>,
plots: PlotDef[],
): PlotDef[] {
return plots.map((plot, i) => ({
...plot,
title: formatSmaMaDisplayLabel(params, i + 1),
}));
}
export function createDefaultSmaPlots(): PlotDef[] {
return SMA_DEFAULT_PERIODS.map((period, i) => ({
id: smaPlotId(i),
title: formatSmaPeriodDisplayLabel(period),
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: formatSmaMaDisplayLabel(params, 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;
}