/** 전략 조건 DSL — 보조지표 DB 설정과 임계값·기간 상inherit 정규화 */ import type { ConditionDSL, LogicNode } from './strategyTypes'; import type { DefType } from './strategyEditorShared'; import { getDefaultConditionFields, getRightFieldOpts } from './strategyEditorShared'; import { ensureDirectThresholdSnapshot, isThresholdOverridden, isValuePeriodOverridden, usesValuePeriodField, VALUE_FIELD_PREFIX, } from './conditionPeriods'; import { inferThresholdSymbolFromLegacyExact, isThresholdSymbol, } from './thresholdSymbols'; import { parseThresholdField, setDirectThreshold } from './conditionPeriods'; import { repairPriceExtremePairForest } from './priceExtremeBreakoutPair'; function isConditionCarrier(node: LogicNode): node is LogicNode & { condition: ConditionDSL } { return !!node.condition && (node.type === 'CONDITION' || node.type == null || node.type === undefined); } function migrateConditionThreshold( cond: ConditionDSL, def: DefType, signalType: 'buy' | 'sell', ): ConditionDSL { // 직접입력(K_55 등) — 숫자 스냅샷 유지, HL_* 로 접지 않음 if (isThresholdOverridden(cond)) { return cond; } let rightField = cond.rightField; if (rightField?.startsWith('K_')) { const exact = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, def.hlThresh); if (exact) { rightField = exact; } else { // K_55 등 비표준값 — 가장 가까운 HL_* 로 접지 않고 숫자 스냅샷 유지 const val = parseThresholdField(rightField); if (val != null) { return migrateConditionPeriod(setDirectThreshold(cond, 'right', val)); } } } if (!rightField) { rightField = getDefaultConditionFields(cond.indicatorType, signalType, def).r; } else if (!isThresholdSymbol(rightField) && !rightField.startsWith('K_')) { const rightOpts = getRightFieldOpts(cond.indicatorType, signalType, def, cond); const isValidRight = rightOpts.some(o => o.value === rightField); if (!isValidRight) { rightField = getDefaultConditionFields(cond.indicatorType, signalType, def).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 (isConditionCarrier(node)) { next = { ...node, type: node.type ?? 'CONDITION', condition: migrateConditionForEditor(node.condition, def, signalType), }; } if (next.children?.length) { next = { ...next, children: next.children.map(c => migrateLogicNode(c, def, signalType)) }; } const legacyChild = (next as LogicNode & { child?: LogicNode }).child; if (legacyChild) { next = { ...next, child: migrateLogicNode(legacyChild, def, signalType) } as LogicNode; } return next; } /** 전략 로드·편집기 진입 시 레거시 K_ 스냅샷 → hline 역할 심볼 */ export function migrateLogicRootForEditor( root: LogicNode | null, orphans: LogicNode[], def: DefType, signalType: 'buy' | 'sell', ): { root: LogicNode | null; orphans: LogicNode[] } { const migrated = { root: root ? migrateLogicNode(root, def, signalType) : null, orphans: orphans.map(n => migrateLogicNode(n, def, signalType)), }; return repairPriceExtremePairForest(migrated.root, migrated.orphans); } /** 저장·표시·평가 공통 — K_* 직접입력 스냅샷 보강 (HL_* 치환 없음) */ export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL { let next = ensureDirectThresholdSnapshot(cond); 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: LogicNode = node; if (isConditionCarrier(node)) { next = { ...node, type: node.type ?? 'CONDITION', condition: normalizeConditionForPersistence(node.condition), }; } if (next.children?.length) { next = { ...next, children: next.children.map(normalizeLogicNode) }; } const legacyChild = (next as LogicNode & { child?: LogicNode }).child; if (legacyChild) { next = { ...next, child: normalizeLogicNode(legacyChild) } as LogicNode; } return next; } export function normalizeLogicRootForPersistence(root: LogicNode | null): LogicNode | null { return root ? normalizeLogicNode(root) : null; }