일목균형표 수정

This commit is contained in:
Macbook
2026-05-27 23:36:48 +09:00
parent 9cee6387c3
commit 8cc0d1c88c
73 changed files with 2256 additions and 334 deletions
+121 -38
View File
@@ -5,8 +5,10 @@ import {
DEFAULT_START_CANDLE,
DEFAULT_START_COMBINE_OP,
START_NODE_ID,
STRATEGY_CANDLE_TYPES,
createStartNodeId,
defaultStartMeta,
getStartCandleTypes,
normalizeStartCandleType,
type StartCombineOp,
type StartNodeMeta,
@@ -25,30 +27,42 @@ export type { StartCombineOp };
export type ConditionBranch = {
candleType: string;
candleTypes?: string[];
root: LogicNode | null;
};
export type StartSection = {
startId: string;
candleType: string;
candleTypes: string[];
root: LogicNode | null;
canDelete: boolean;
};
function metaToStartSection(
startId: string,
meta: Record<string, StartNodeMeta>,
root: LogicNode | null,
canDelete: boolean,
): StartSection {
const candleTypes = getStartCandleTypes(meta[startId]);
return {
startId,
candleType: candleTypes[0],
candleTypes,
root,
canDelete,
};
}
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,
}];
const sections: StartSection[] = [
metaToStartSection(START_NODE_ID, state.startMeta, state.root, false),
];
for (const startId of state.extraStartIds) {
sections.push({
startId,
candleType: normalizeStartCandleType(state.startMeta[startId]?.candleType),
root: state.extraRoots[startId] ?? null,
canDelete: true,
});
sections.push(
metaToStartSection(startId, state.startMeta, state.extraRoots[startId] ?? null, true),
);
}
return sections;
}
@@ -110,8 +124,12 @@ function syncSubtreeCandleTypes(node: LogicNode | null, candleType: string): Log
function collectExplicitCandleTypesFromTree(node: LogicNode | null, out: Set<string>): void {
if (!node) return;
if (node.type === 'TIMEFRAME' && node.candleType) {
out.add(normalizeStartCandleType(node.candleType));
if (node.type === 'TIMEFRAME') {
if (node.candleTypes?.length) {
for (const ct of node.candleTypes) out.add(normalizeStartCandleType(ct));
} else if (node.candleType) {
out.add(normalizeStartCandleType(node.candleType));
}
}
if (node.type === 'CONDITION' && node.condition) {
const c = node.condition as ConditionDSL;
@@ -130,22 +148,23 @@ function inferPrimaryCandleFromTree(root: LogicNode | null): string {
return DEFAULT_START_CANDLE;
}
export function updateStartCandleType(
export function updateStartCandleTypes(
state: EditorConditionState,
startId: string,
candleType: string,
candleTypes: string[],
): EditorConditionState {
const ct = normalizeStartCandleType(candleType);
const types = normalizeCandleTypesList(candleTypes);
const primary = types[0];
const nextMeta = {
...state.startMeta,
[startId]: { candleType: ct },
[startId]: { candleTypes: types, candleType: primary },
};
if (startId === START_NODE_ID) {
return {
...state,
startMeta: nextMeta,
root: syncSubtreeCandleTypes(state.root, ct),
root: syncSubtreeCandleTypes(state.root, primary),
};
}
@@ -154,11 +173,39 @@ export function updateStartCandleType(
startMeta: nextMeta,
extraRoots: {
...state.extraRoots,
[startId]: syncSubtreeCandleTypes(state.extraRoots[startId] ?? null, ct),
[startId]: syncSubtreeCandleTypes(state.extraRoots[startId] ?? null, primary),
},
};
}
/** @deprecated 단일 분봉 — updateStartCandleTypes 사용 */
export function updateStartCandleType(
state: EditorConditionState,
startId: string,
candleType: string,
): EditorConditionState {
return updateStartCandleTypes(state, startId, [candleType]);
}
function normalizeCandleTypesList(types: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const ct of STRATEGY_CANDLE_TYPES) {
if (types.some(t => normalizeStartCandleType(t) === ct) && !seen.has(ct)) {
seen.add(ct);
out.push(ct);
}
}
return out.length > 0 ? out : [DEFAULT_START_CANDLE];
}
function readTimeframeCandleTypes(node: LogicNode): string[] {
if (node.candleTypes?.length) {
return normalizeCandleTypesList(node.candleTypes);
}
return [normalizeStartCandleType(node.candleType)];
}
export function addExtraStartSection(state: EditorConditionState): EditorConditionState {
const startId = createStartNodeId();
return {
@@ -166,7 +213,7 @@ export function addExtraStartSection(state: EditorConditionState): EditorConditi
extraStartIds: [...state.extraStartIds, startId],
startMeta: {
...state.startMeta,
[startId]: { candleType: DEFAULT_START_CANDLE },
[startId]: { candleTypes: [DEFAULT_START_CANDLE], candleType: DEFAULT_START_CANDLE },
},
extraRoots: { ...state.extraRoots, [startId]: null },
};
@@ -206,32 +253,48 @@ export function emptyEditorConditionState(): EditorConditionState {
}
function wrapTimeframe(candleType: string, root: LogicNode): LogicNode {
const ct = normalizeStartCandleType(candleType);
return {
id: generateNodeId(),
type: 'TIMEFRAME',
candleType: normalizeStartCandleType(candleType),
candleType: ct,
children: [root],
};
}
function wrapTimeframes(candleTypes: string[], root: LogicNode): LogicNode {
const types = normalizeCandleTypesList(candleTypes);
if (types.length === 1) return wrapTimeframe(types[0], root);
return {
id: generateNodeId(),
type: 'TIMEFRAME',
candleType: types[0],
candleTypes: types,
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;
}>;
const branches = collectEditorBranches(state).filter(
(b): b is ConditionBranch & { root: LogicNode } => b.root != null,
);
if (branches.length === 0) return null;
if (branches.length === 1) {
const { candleType, root } = branches[0];
return wrapTimeframe(candleType, root);
const { candleType, candleTypes, root } = branches[0];
const types = candleTypes?.length ? candleTypes : [candleType];
return wrapTimeframes(types, root);
}
const combineOp = normalizeStartCombineOp(state.startCombineOp);
return {
id: generateNodeId(),
type: combineOp,
children: branches.map(b => wrapTimeframe(b.candleType, b.root)),
children: branches.map(b => {
const types = b.candleTypes?.length ? b.candleTypes : [b.candleType];
return wrapTimeframes(types, b.root);
}),
};
}
@@ -247,15 +310,16 @@ function parseMultiStartWrapper(dsl: LogicNode): EditorConditionState | null {
let primaryRoot: LogicNode | null = null;
children.forEach((tf, index) => {
const candleType = normalizeStartCandleType(tf.candleType);
const candleTypes = readTimeframeCandleTypes(tf as LogicNode);
const candleType = candleTypes[0];
const branchRoot = tf.children?.[0] ?? null;
if (index === 0) {
startMeta[START_NODE_ID] = { candleType };
startMeta[START_NODE_ID] = { candleTypes, candleType };
primaryRoot = branchRoot;
return;
}
const startId = createStartNodeId();
startMeta[startId] = { candleType };
startMeta[startId] = { candleTypes, candleType };
extraStartIds.push(startId);
extraRoots[startId] = branchRoot;
});
@@ -277,10 +341,11 @@ export function decodeConditionForEditor(dsl: LogicNode | null): EditorCondition
if (multiStart) return multiStart;
if (dsl.type === 'TIMEFRAME') {
const candleTypes = readTimeframeCandleTypes(dsl);
return {
root: dsl.children?.[0] ?? null,
startMeta: {
[START_NODE_ID]: { candleType: normalizeStartCandleType(dsl.candleType) },
[START_NODE_ID]: { candleTypes, candleType: candleTypes[0] },
},
extraStartIds: [],
extraRoots: {},
@@ -292,7 +357,7 @@ export function decodeConditionForEditor(dsl: LogicNode | null): EditorCondition
return {
root: dsl,
startMeta: {
[START_NODE_ID]: { candleType: inferred },
[START_NODE_ID]: { candleTypes: [inferred], candleType: inferred },
},
extraStartIds: [],
extraRoots: {},
@@ -303,14 +368,22 @@ export function decodeConditionForEditor(dsl: LogicNode | null): EditorCondition
/** 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 });
const primaryTypes = getStartCandleTypes(state.startMeta[START_NODE_ID]);
if (state.root) {
branches.push({
candleType: primaryTypes[0],
candleTypes: primaryTypes.length > 1 ? primaryTypes : undefined,
root: state.root,
});
}
for (const startId of state.extraStartIds) {
const root = state.extraRoots[startId] ?? null;
if (root) {
const types = getStartCandleTypes(state.startMeta[startId]);
branches.push({
candleType: state.startMeta[startId]?.candleType ?? DEFAULT_START_CANDLE,
candleType: types[0],
candleTypes: types.length > 1 ? types : undefined,
root,
});
}
@@ -318,6 +391,15 @@ export function collectEditorBranches(state: EditorConditionState): ConditionBra
return branches;
}
/** DSL·UI에서 사용하는 전략 평가 분봉 목록 */
export function collectTimeframesFromEditorState(state: EditorConditionState): string[] {
const set = new Set<string>();
for (const section of collectStartSections(state)) {
for (const ct of section.candleTypes) set.add(ct);
}
return [...set];
}
/** 저장된 DSL에서 Logic Expression용 분기 목록 */
export function collectDslBranches(dsl: LogicNode | null): ConditionBranch[] {
return collectEditorBranches(decodeConditionForEditor(dsl));
@@ -335,7 +417,8 @@ export function mergeStartMetaForLoad(
...stored,
};
for (const [startId, meta] of Object.entries(decoded)) {
merged[startId] = { candleType: normalizeStartCandleType(meta.candleType) };
const types = getStartCandleTypes(meta);
merged[startId] = { candleTypes: types, candleType: types[0] };
}
return merged;
}