/** 전략 조건 DSL — 보조지표 DB 설정과 임계값·기간 상inherit 정규화 */ import type { ConditionDSL, LogicNode } from './strategyTypes'; import type { DefType } from './strategyEditorShared'; import { getDefaultConditionFields } from './strategyEditorShared'; import { isThresholdOverridden, isValuePeriodOverridden, parseThresholdField, usesValuePeriodField, VALUE_FIELD_PREFIX, } from './conditionPeriods'; import { defaultInheritedThresholdField, inferThresholdSymbolFromLegacy, isThresholdFieldOrSymbol, isThresholdSymbol, THRESHOLD_HL_MID, THRESHOLD_HL_OVER, } from './thresholdSymbols'; function migrateConditionThreshold( cond: ConditionDSL, def: DefType, signalType: 'buy' | 'sell', ): ConditionDSL { if (isThresholdOverridden(cond)) return cond; let rightField = cond.rightField; if (rightField?.startsWith('K_')) { rightField = inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, def.hlThresh); } if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) { const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def); if (isThresholdFieldOrSymbol(defaults.r)) { rightField = defaults.r; } } const next: ConditionDSL = { ...cond, rightField, thresholdOverride: false, }; const { targetValue: _t, ...rest } = next; return rest; } function migrateConditionPeriod(cond: ConditionDSL): ConditionDSL { if (isValuePeriodOverridden(cond)) return cond; const prefix = VALUE_FIELD_PREFIX[cond.indicatorType]; if (!prefix || !usesValuePeriodField(cond)) return cond; const lf = cond.leftField ?? ''; if (lf.startsWith(`${prefix}_`)) { const { period: _p, ...rest } = cond; return { ...rest, leftField: prefix, valuePeriodOverride: false }; } return cond; } export function migrateConditionForEditor( cond: ConditionDSL, def: DefType, signalType: 'buy' | 'sell', ): ConditionDSL { return migrateConditionPeriod(migrateConditionThreshold(cond, def, signalType)); } function migrateLogicNode( node: LogicNode, def: DefType, signalType: 'buy' | 'sell', ): LogicNode { let next = node; if (node.type === 'CONDITION' && node.condition) { next = { ...node, condition: migrateConditionForEditor(node.condition, def, signalType) }; } if (next.children?.length) { next = { ...next, children: next.children.map(c => migrateLogicNode(c, def, signalType)) }; } return next; } /** 전략 로드·편집기 진입 시 레거시 K_ 스냅샷 → hline 역할 심볼 */ export function migrateLogicRootForEditor( root: LogicNode | null, orphans: LogicNode[], def: DefType, signalType: 'buy' | 'sell', ): { root: LogicNode | null; orphans: LogicNode[] } { return { root: root ? migrateLogicNode(root, def, signalType) : null, orphans: orphans.map(n => migrateLogicNode(n, def, signalType)), }; } /** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거 */ export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL { let next: ConditionDSL = { ...cond }; if (!isThresholdOverridden(next)) { if (next.rightField?.startsWith('K_')) { next.rightField = defaultInheritedThresholdField(next.indicatorType); } if (!isThresholdSymbol(next.rightField)) { const role = next.indicatorType === 'DISPARITY' || next.indicatorType === 'VOLUME_OSC' || next.indicatorType === 'TRIX' ? THRESHOLD_HL_MID : THRESHOLD_HL_OVER; if (parseThresholdField(next.rightField) != null || !next.rightField) { next.rightField = role; } } const { targetValue: _t, ...rest } = next; next = { ...rest, thresholdOverride: false }; } if (!isValuePeriodOverridden(next)) { const prefix = VALUE_FIELD_PREFIX[next.indicatorType]; if (prefix && usesValuePeriodField(next)) { next.leftField = prefix; } const { period: _p, ...rest } = next; next = rest; } return next; } function normalizeLogicNode(node: LogicNode): LogicNode { let next = node; if (node.type === 'CONDITION' && node.condition) { next = { ...node, condition: normalizeConditionForPersistence(node.condition) }; } if (next.children?.length) { next = { ...next, children: next.children.map(normalizeLogicNode) }; } return next; } export function normalizeLogicRootForPersistence(root: LogicNode | null): LogicNode | null { return root ? normalizeLogicNode(root) : null; }