다중 시간봉 전략 설정기능 추가

This commit is contained in:
Macbook
2026-05-25 02:49:39 +09:00
parent cc01f311bc
commit 30dedc4abc
17 changed files with 1903 additions and 249 deletions
@@ -0,0 +1,259 @@
import type { LogicNode } from './strategyTypes';
import { generateNodeId } from './strategyTypes';
import { subtreeToFlatOrphans } from './strategyFlowLayout';
import {
DEFAULT_START_CANDLE,
DEFAULT_START_COMBINE_OP,
START_NODE_ID,
createStartNodeId,
defaultStartMeta,
normalizeStartCandleType,
type StartCombineOp,
type StartNodeMeta,
} from './strategyStartNodes';
export type EditorConditionState = {
root: LogicNode | null;
startMeta: Record<string, StartNodeMeta>;
extraStartIds: string[];
extraRoots: Record<string, LogicNode | null>;
/** START 2개 이상일 때 분기 결합 (기본 AND) */
startCombineOp?: StartCombineOp;
};
export type { StartCombineOp };
export type ConditionBranch = {
candleType: string;
root: LogicNode | null;
};
export type StartSection = {
startId: string;
candleType: string;
root: LogicNode | null;
canDelete: boolean;
};
export function collectStartSections(state: EditorConditionState): StartSection[] {
const sections: StartSection[] = [{
startId: START_NODE_ID,
candleType: normalizeStartCandleType(state.startMeta[START_NODE_ID]?.candleType),
root: state.root,
canDelete: false,
}];
for (const startId of state.extraStartIds) {
sections.push({
startId,
candleType: normalizeStartCandleType(state.startMeta[startId]?.candleType),
root: state.extraRoots[startId] ?? null,
canDelete: true,
});
}
return sections;
}
export function hasMultipleStartSections(state: EditorConditionState): boolean {
return state.extraStartIds.length >= 1;
}
export function normalizeStartCombineOp(value: string | undefined | null): StartCombineOp {
return value === 'OR' ? 'OR' : DEFAULT_START_COMBINE_OP;
}
export function updateStartCombineOp(
state: EditorConditionState,
op: StartCombineOp,
): EditorConditionState {
return { ...state, startCombineOp: op };
}
export function updateBranchRoot(
state: EditorConditionState,
startId: string,
root: LogicNode | null,
): EditorConditionState {
if (startId === START_NODE_ID) {
return { ...state, root };
}
return {
...state,
extraRoots: { ...state.extraRoots, [startId]: root },
};
}
export function updateStartCandleType(
state: EditorConditionState,
startId: string,
candleType: string,
): EditorConditionState {
return {
...state,
startMeta: {
...state.startMeta,
[startId]: { candleType: normalizeStartCandleType(candleType) },
},
};
}
export function addExtraStartSection(state: EditorConditionState): EditorConditionState {
const startId = createStartNodeId();
return {
...state,
extraStartIds: [...state.extraStartIds, startId],
startMeta: {
...state.startMeta,
[startId]: { candleType: DEFAULT_START_CANDLE },
},
extraRoots: { ...state.extraRoots, [startId]: null },
};
}
export function removeStartSection(
state: EditorConditionState,
startId: string,
): { state: EditorConditionState; orphanedNodes: LogicNode[] } {
if (startId === START_NODE_ID) {
return { state, orphanedNodes: [] };
}
const branch = state.extraRoots[startId];
const nextMeta = { ...state.startMeta };
delete nextMeta[startId];
const nextRoots = { ...state.extraRoots };
delete nextRoots[startId];
return {
state: {
...state,
startMeta: nextMeta,
extraStartIds: state.extraStartIds.filter(id => id !== startId),
extraRoots: nextRoots,
},
orphanedNodes: branch ? subtreeToFlatOrphans(branch) : [],
};
}
export function emptyEditorConditionState(): EditorConditionState {
return {
root: null,
startMeta: defaultStartMeta(),
extraStartIds: [],
extraRoots: {},
startCombineOp: DEFAULT_START_COMBINE_OP,
};
}
function wrapTimeframe(candleType: string, root: LogicNode): LogicNode {
return {
id: generateNodeId(),
type: 'TIMEFRAME',
candleType: normalizeStartCandleType(candleType),
children: [root],
};
}
/** 편집기 상태 → 저장/평가용 DSL (TIMEFRAME 래핑) */
export function encodeConditionForSave(state: EditorConditionState): LogicNode | null {
const branches = collectEditorBranches(state).filter(b => b.root != null) as Array<{
candleType: string;
root: LogicNode;
}>;
if (branches.length === 0) return null;
if (branches.length === 1) {
const { candleType, root } = branches[0];
if (normalizeStartCandleType(candleType) === DEFAULT_START_CANDLE) return root;
return wrapTimeframe(candleType, root);
}
const combineOp = normalizeStartCombineOp(state.startCombineOp);
return {
id: generateNodeId(),
type: combineOp,
children: branches.map(b => wrapTimeframe(b.candleType, b.root)),
};
}
function parseMultiStartWrapper(dsl: LogicNode): EditorConditionState | null {
if (dsl.type !== 'AND' && dsl.type !== 'OR') return null;
const children = dsl.children ?? [];
const allTimeframe = children.length > 0 && children.every(c => c.type === 'TIMEFRAME');
if (!allTimeframe) return null;
const startMeta: Record<string, StartNodeMeta> = {};
const extraStartIds: string[] = [];
const extraRoots: Record<string, LogicNode | null> = {};
let primaryRoot: LogicNode | null = null;
children.forEach((tf, index) => {
const candleType = normalizeStartCandleType(tf.candleType);
const branchRoot = tf.children?.[0] ?? null;
if (index === 0) {
startMeta[START_NODE_ID] = { candleType };
primaryRoot = branchRoot;
return;
}
const startId = createStartNodeId();
startMeta[startId] = { candleType };
extraStartIds.push(startId);
extraRoots[startId] = branchRoot;
});
return {
root: primaryRoot,
startMeta,
extraStartIds,
extraRoots,
startCombineOp: dsl.type as StartCombineOp,
};
}
/** DSL → 편집기 상태 (TIMEFRAME / AND|OR+TIMEFRAME 분해) */
export function decodeConditionForEditor(dsl: LogicNode | null): EditorConditionState {
if (!dsl) return emptyEditorConditionState();
const multiStart = parseMultiStartWrapper(dsl);
if (multiStart) return multiStart;
if (dsl.type === 'TIMEFRAME') {
return {
root: dsl.children?.[0] ?? null,
startMeta: {
[START_NODE_ID]: { candleType: normalizeStartCandleType(dsl.candleType) },
},
extraStartIds: [],
extraRoots: {},
startCombineOp: DEFAULT_START_COMBINE_OP,
};
}
return {
root: dsl,
startMeta: defaultStartMeta(),
extraStartIds: [],
extraRoots: {},
startCombineOp: DEFAULT_START_COMBINE_OP,
};
}
/** Logic Expression / 미리보기용 — START별 평가 분봉과 서브트리 */
export function collectEditorBranches(state: EditorConditionState): ConditionBranch[] {
const branches: ConditionBranch[] = [];
const primaryCt = state.startMeta[START_NODE_ID]?.candleType ?? DEFAULT_START_CANDLE;
if (state.root) branches.push({ candleType: primaryCt, root: state.root });
for (const startId of state.extraStartIds) {
const root = state.extraRoots[startId] ?? null;
if (root) {
branches.push({
candleType: state.startMeta[startId]?.candleType ?? DEFAULT_START_CANDLE,
root,
});
}
}
return branches;
}
/** 저장된 DSL에서 Logic Expression용 분기 목록 */
export function collectDslBranches(dsl: LogicNode | null): ConditionBranch[] {
return collectEditorBranches(decodeConditionForEditor(dsl));
}