다중 시간봉 전략 설정기능 추가
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user