160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
/** 전략 조건 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;
|
|
}
|