전략평가 MA 참조에러 수정
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* MA/EMA DSL 필드 — MA1~MA11 슬롯 ↔ SMA period1~11 (보조지표 설정과 동일)
|
||||
*
|
||||
* - MA3 → period3 (예: 20일)
|
||||
* - MA20 → 레거시: 숫자>11 이면 기간 20일 그대로
|
||||
*/
|
||||
import { SMA_DEFAULT_PERIODS, SMA_MA_COUNT } from './smaConfig';
|
||||
|
||||
export const MA_DSL_SLOT_MAX = SMA_MA_COUNT;
|
||||
|
||||
export function buildMaLinesFromSmaParams(
|
||||
params?: Record<string, number | string | boolean>,
|
||||
): number[] {
|
||||
const lines: number[] = [];
|
||||
for (let slot = 1; slot <= SMA_MA_COUNT; slot++) {
|
||||
const raw = params?.[`period${slot}`];
|
||||
const p = typeof raw === 'number' && raw > 0
|
||||
? Math.floor(raw)
|
||||
: SMA_DEFAULT_PERIODS[slot - 1];
|
||||
lines.push(p);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** DSL MA/EMA 필드 → 실제 SMA/EMA 계산 기간 */
|
||||
export function resolveMaDslFieldPeriod(
|
||||
field: string | undefined,
|
||||
smaParams?: Record<string, number | string | boolean>,
|
||||
): number | null {
|
||||
if (!field) return null;
|
||||
const ma = /^MA(\d+)$/.exec(field);
|
||||
if (ma && !field.startsWith('MACD')) {
|
||||
const n = parseInt(ma[1], 10);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
if (n <= MA_DSL_SLOT_MAX) return buildMaLinesFromSmaParams(smaParams)[n - 1];
|
||||
return n;
|
||||
}
|
||||
const ema = /^EMA(\d+)$/.exec(field);
|
||||
if (ema) {
|
||||
const n = parseInt(ema[1], 10);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
if (n <= MA_DSL_SLOT_MAX) return buildMaLinesFromSmaParams(smaParams)[n - 1];
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** UI·해석용 라벨 — MA5(60일) */
|
||||
export function formatMaDslFieldLabel(
|
||||
field: string | undefined,
|
||||
smaParams?: Record<string, number | string | boolean>,
|
||||
): string {
|
||||
if (!field) return '';
|
||||
const period = resolveMaDslFieldPeriod(field, smaParams);
|
||||
if (period == null) return field;
|
||||
const ma = /^MA(\d+)$/.exec(field);
|
||||
if (ma && !field.startsWith('MACD')) {
|
||||
const n = parseInt(ma[1], 10);
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return `MA${n}(${period}일)`;
|
||||
return `MA(${period}일)`;
|
||||
}
|
||||
const ema = /^EMA(\d+)$/.exec(field);
|
||||
if (ema) {
|
||||
const n = parseInt(ema[1], 10);
|
||||
if (n >= 1 && n <= MA_DSL_SLOT_MAX) return `EMA${n}(${period}일)`;
|
||||
return `EMA(${period}일)`;
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
export function buildMaFieldOptions(
|
||||
maLines: number[],
|
||||
): { value: string; label: string }[] {
|
||||
return Array.from({ length: SMA_MA_COUNT }, (_, i) => {
|
||||
const slot = i + 1;
|
||||
const p = maLines[i] ?? SMA_DEFAULT_PERIODS[i];
|
||||
return { value: `MA${slot}`, label: `MA${slot}(${p}일)` };
|
||||
});
|
||||
}
|
||||
|
||||
export function buildEmaFieldOptions(
|
||||
maLines: number[],
|
||||
maxSlots = 4,
|
||||
): { value: string; label: string }[] {
|
||||
const n = Math.min(maxSlots, SMA_MA_COUNT);
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const slot = i + 1;
|
||||
const p = maLines[i] ?? SMA_DEFAULT_PERIODS[i];
|
||||
return { value: `EMA${slot}`, label: `EMA${slot}(${p}일)` };
|
||||
});
|
||||
}
|
||||
|
||||
/** 레거시 MA20·MA60(기간 인코딩) 옵션 유지 */
|
||||
export function appendLegacyMaFieldOption(
|
||||
opts: { value: string; label: string }[],
|
||||
field: string | undefined,
|
||||
): { value: string; label: string }[] {
|
||||
if (!field || opts.some(o => o.value === field)) return opts;
|
||||
const period = resolveMaDslFieldPeriod(field);
|
||||
if (period == null) return opts;
|
||||
const ma = /^MA(\d+)$/.exec(field);
|
||||
if (ma && !field.startsWith('MACD')) {
|
||||
const n = parseInt(ma[1], 10);
|
||||
if (n > MA_DSL_SLOT_MAX) {
|
||||
return [...opts, { value: field, label: `MA(${period}일)` }];
|
||||
}
|
||||
}
|
||||
const ema = /^EMA(\d+)$/.exec(field);
|
||||
if (ema) {
|
||||
const n = parseInt(ema[1], 10);
|
||||
if (n > MA_DSL_SLOT_MAX) {
|
||||
return [...opts, { value: field, label: `EMA(${period}일)` }];
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
candleRangeWindowBars,
|
||||
formatCandleRangeClause,
|
||||
} from '../utils/strategyTypes';
|
||||
import {
|
||||
appendLegacyMaFieldOption,
|
||||
buildEmaFieldOptions,
|
||||
buildMaFieldOptions,
|
||||
} from './maDslField';
|
||||
import {
|
||||
COMPOSITE_INDICATOR_ITEMS,
|
||||
compositeDisplayName,
|
||||
@@ -136,7 +141,7 @@ export const saveStratsLocal = (_s: StrategyDto[]) => { /* no-op */ };
|
||||
interface HLThresh { over?: number; mid?: number; under?: number; }
|
||||
|
||||
/** 하드코딩 기본값 — activeIndicators가 없을 때 폴백 */
|
||||
const DEF_DEFAULTS = {
|
||||
export const DEF_DEFAULTS = {
|
||||
rsiPeriod: 9,
|
||||
macdFast: 12, macdSlow: 26, macdSignal: 9,
|
||||
cciPeriod: 13,
|
||||
@@ -347,6 +352,23 @@ export function buildStrategyEditorDefFromSettings(
|
||||
}
|
||||
|
||||
/** @deprecated buildStrategyEditorDefFromSettings 사용 — 하드코딩 폴백 */
|
||||
export function buildDefFromGetParams(
|
||||
getParams: (type: string, defaults?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
|
||||
): DefType {
|
||||
const types = [
|
||||
'RSI', 'MACD', 'CCI', 'Stochastic', 'ADX', 'TRIX', 'DMI', 'BollingerBands',
|
||||
'IchimokuCloud', 'WilliamsPercentRange', 'VolumeOscillator', 'VR', 'Disparity',
|
||||
'Psychological', 'NewPsychological', 'InvestPsychological', 'OBV', 'SMA', 'EMA',
|
||||
] as const;
|
||||
const configs: IndicatorConfig[] = types.map(type => ({
|
||||
id: `eval-def-${type}`,
|
||||
type,
|
||||
params: getParams(type, getIndicatorDef(type)?.defaultParams as Record<string, number | string | boolean>),
|
||||
plots: getIndicatorDef(type)?.plots ?? [],
|
||||
}));
|
||||
return buildDef(configs);
|
||||
}
|
||||
|
||||
export function buildStrategyEditorDef(): DefType {
|
||||
return buildDef([]);
|
||||
}
|
||||
@@ -497,16 +519,26 @@ export const getFieldOpts = (
|
||||
{ value:'VOLUME_VALUE', label:'거래량' },
|
||||
{ value:'VOLUME_MA', label:'거래량 이동평균' },
|
||||
]);
|
||||
case 'MA': return [
|
||||
NONE,
|
||||
...DEF.maLines.map((p, i) => ({ value:`MA${p}`, label:`MA${i+1}(${p}일)` })),
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
];
|
||||
case 'EMA': return [
|
||||
NONE,
|
||||
...DEF.maLines.slice(0,4).map((p, i) => ({ value:`EMA${p}`, label:`EMA${i+1}(${p}일)` })),
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
];
|
||||
case 'MA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...buildMaFieldOptions(DEF.maLines),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
|
||||
if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
|
||||
return opts;
|
||||
}
|
||||
case 'EMA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...buildEmaFieldOptions(DEF.maLines, 4),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
|
||||
if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
|
||||
return opts;
|
||||
}
|
||||
case 'DISPARITY': {
|
||||
const { mid } = th({ mid: 100 });
|
||||
return condOpts([
|
||||
@@ -667,8 +699,8 @@ const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: D
|
||||
DMI: { l:'PDI', r:'MDI' },
|
||||
OBV: { l:'OBV_LINE', r:'OBV_SIGNAL' },
|
||||
VOLUME: { l:'VOLUME_VALUE', r:'VOLUME_MA' },
|
||||
MA: { l:`MA${DEF.maLines[0]}`, r:`MA${DEF.maLines[1]}` },
|
||||
EMA: { l:`EMA${DEF.maLines[0]}`, r:`EMA${DEF.maLines[1]}` },
|
||||
MA: { l: 'MA1', r: 'MA2' },
|
||||
EMA: { l: 'EMA1', r: 'EMA2' },
|
||||
DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: THRESHOLD_HL_MID },
|
||||
PSYCHOLOGICAL: { l:'PSY_VALUE', r: THRESHOLD_HL_OVER },
|
||||
NEW_PSYCHOLOGICAL: { l:'NEW_PSY_VALUE', r: THRESHOLD_HL_OVER },
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
formatStrategyThreshold,
|
||||
type ConditionMetric,
|
||||
} from './virtualSignalMetrics';
|
||||
import { nodeToText } from './strategyEditorShared';
|
||||
import { nodeToText, buildDefFromGetParams, type DefType, DEF_DEFAULTS } from './strategyEditorShared';
|
||||
|
||||
export interface ConditionInterpretContext {
|
||||
condition: ConditionDSL;
|
||||
@@ -49,23 +49,24 @@ function walkContext(
|
||||
side: 'buy' | 'sell',
|
||||
out: Map<string, ConditionInterpretContext>,
|
||||
seen: Set<string>,
|
||||
def: DefType,
|
||||
): void {
|
||||
if (!node) return;
|
||||
|
||||
if (node.type === 'TIMEFRAME') {
|
||||
const tf = normalizeStartCandleType(node.candleTypes?.[0] ?? node.candleType ?? timeframe);
|
||||
node.children?.forEach(c => walkContext(c, tf, side, out, seen));
|
||||
node.children?.forEach(c => walkContext(c, tf, side, out, seen, def));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'NOT') {
|
||||
const child = node.children?.[0] ?? (node as LogicNode & { child?: LogicNode }).child;
|
||||
if (child) walkContext(child, timeframe, side, out, seen);
|
||||
if (child) walkContext(child, timeframe, side, out, seen, def);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'AND' || node.type === 'OR') {
|
||||
node.children?.forEach(c => walkContext(c, timeframe, side, out, seen));
|
||||
node.children?.forEach(c => walkContext(c, timeframe, side, out, seen, def));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,22 +78,24 @@ function walkContext(
|
||||
seen.add(rowId);
|
||||
out.set(rowId, {
|
||||
condition: c,
|
||||
summary: nodeToText(node),
|
||||
summary: nodeToText(node, def),
|
||||
timeframe: rowTf,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
node.children?.forEach(c => walkContext(c, timeframe, side, out, seen));
|
||||
node.children?.forEach(c => walkContext(c, timeframe, side, out, seen, def));
|
||||
}
|
||||
|
||||
export function buildConditionContextMap(
|
||||
strategy: StrategyDto | null | undefined,
|
||||
getParams?: (type: string, defaults?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
|
||||
): Map<string, ConditionInterpretContext> {
|
||||
const def = getParams ? buildDefFromGetParams(getParams) : DEF_DEFAULTS;
|
||||
const out = new Map<string, ConditionInterpretContext>();
|
||||
const seen = new Set<string>();
|
||||
walkContext(asLogicNode(strategy?.buyCondition), '1m', 'buy', out, seen);
|
||||
walkContext(asLogicNode(strategy?.sellCondition), '1m', 'sell', out, seen);
|
||||
walkContext(asLogicNode(strategy?.buyCondition), '1m', 'buy', out, seen, def);
|
||||
walkContext(asLogicNode(strategy?.sellCondition), '1m', 'sell', out, seen, def);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user