일목균형표 수정
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user