전략조건 설명 오류 수정

This commit is contained in:
Macbook
2026-06-19 01:32:43 +09:00
parent 5bbc105326
commit 8bceadc55b
@@ -215,16 +215,97 @@ function describeCondition(
return `${left}에 대해 「${label}」 조건이 성립하는 경우`; return `${left}에 대해 「${label}」 조건이 성립하는 경우`;
} }
function describeNode( function describeConditionNode(
node: LogicNode, node: LogicNode,
signalType: 'buy' | 'sell', signalType: 'buy' | 'sell',
def: DefType, def: DefType,
depth = 0, ): string {
if (node.type !== 'CONDITION' || !node.condition) return '';
return describeCondition(normalizeCompositeCondition(node.condition), signalType, def);
}
/** AND 그룹 — 자식이 모두 단일 조건이면 묶음으로, 아니면 하위 트리 재귀 */
function describeAndGroup(
children: LogicNode[],
signalType: 'buy' | 'sell',
def: DefType,
indent: string,
options?: { showCountHeader?: boolean },
): string[] {
if (children.length === 0) return [`${indent}(비어 있는 AND 그룹)`];
if (children.length === 1) {
return describeLogicTree(children[0], signalType, def, indent, false);
}
const allSimple = children.every(c => c.type === 'CONDITION');
const lines: string[] = [];
if (options?.showCountHeader !== false && allSimple) {
lines.push(`${indent}(아래 ${children.length}개 조건을 모두 만족)`);
} else if (!allSimple) {
lines.push(`${indent}다음을 모두 동시에 만족:`);
}
if (allSimple) {
for (const child of children) {
const text = describeConditionNode(child, signalType, def);
if (text) lines.push(`${indent}${text}`);
}
return lines;
}
for (const child of children) {
lines.push(...describeLogicTree(child, signalType, def, indent + ' ', false));
}
return lines;
}
/** OR 그룹 — 각 자식을 [경로 N]으로 구분, AND 자식은 묶음으로 표현 */
function describeOrGroup(
children: LogicNode[],
signalType: 'buy' | 'sell',
def: DefType,
indent: string,
atRoot: boolean,
): string[] {
if (children.length === 0) return [`${indent}(비어 있는 OR 그룹)`];
if (children.length === 1) {
return describeLogicTree(children[0], signalType, def, indent, atRoot);
}
const lines: string[] = [];
if (atRoot) {
lines.push('다음 경로 중 하나가 충족되면 됩니다.');
} else {
lines.push(`${indent}다음 중 하나:`);
}
children.forEach((child, idx) => {
if (idx > 0) lines.push(`${indent}또는`);
lines.push(`${indent}[경로 ${idx + 1}]`);
if (child.type === 'AND') {
lines.push(...describeAndGroup(child.children ?? [], signalType, def, indent + ' '));
} else if (child.type === 'CONDITION') {
const text = describeConditionNode(child, signalType, def);
if (text) lines.push(`${indent}${text}`);
} else {
lines.push(...describeLogicTree(child, signalType, def, indent + ' ', false));
}
});
return lines;
}
function describeLogicTree(
node: LogicNode,
signalType: 'buy' | 'sell',
def: DefType,
indent = '',
atRoot = false,
): string[] { ): string[] {
if (node.type === 'CONDITION' && node.condition) { if (node.type === 'CONDITION' && node.condition) {
const c = normalizeCompositeCondition(node.condition); const line = describeCondition(normalizeCompositeCondition(node.condition), signalType, def);
const line = describeCondition(c, signalType, def); return [`${indent}${line}`];
return [depth > 0 ? `${line}` : line];
} }
if (node.type === 'TIMEFRAME') { if (node.type === 'TIMEFRAME') {
@@ -235,47 +316,66 @@ function describeNode(
const tf = types.length > 1 const tf = types.length > 1
? types.map(candleKo).join(', ') ? types.map(candleKo).join(', ')
: candleKo(types[0]); : candleKo(types[0]);
if (!inner) return [`[${tf}] 연결된 조건이 없습니다.`]; if (!inner) return [`${indent}[${tf}] 연결된 조건이 없습니다.`];
const innerLines = describeNode(inner, signalType, def, depth + 1); const innerLines = describeLogicTree(inner, signalType, def, indent + ' ', false);
return [`[${tf} 기준]`, ...innerLines.map(l => (l.startsWith('•') ? ` ${l}` : `${l}`))]; return [`${indent}[${tf} 기준]`, ...innerLines];
} }
if (node.type === 'AND') { if (node.type === 'AND') {
const children = node.children ?? []; const children = node.children ?? [];
if (children.length === 0) return ['(비어 있는 AND 그룹)']; if (children.length === 0) return [`${indent}(비어 있는 AND 그룹)`];
if (children.length === 1) {
return describeLogicTree(children[0], signalType, def, indent, atRoot);
}
const hasOrChild = children.some(c => c.type === 'OR');
if (hasOrChild) {
const lines: string[] = []; const lines: string[] = [];
if (depth === 0 && children.length > 1) { if (atRoot) lines.push('다음 조건을 모두 동시에 만족해야 합니다.');
else lines.push(`${indent}다음을 모두 동시에 만족:`);
for (const child of children) {
lines.push(...describeLogicTree(child, signalType, def, indent + ' ', false));
}
return lines;
}
const lines: string[] = [];
if (atRoot) {
lines.push('다음 조건을 모두 동시에 만족해야 합니다.'); lines.push('다음 조건을 모두 동시에 만족해야 합니다.');
} }
children.forEach(child => { lines.push(...describeAndGroup(children, signalType, def, indent, {
lines.push(...describeNode(child, signalType, def, depth + 1)); showCountHeader: !atRoot,
}); }));
return lines; return lines;
} }
if (node.type === 'OR') { if (node.type === 'OR') {
const children = node.children ?? []; return describeOrGroup(node.children ?? [], signalType, def, indent, atRoot);
if (children.length === 0) return ['(비어 있는 OR 그룹)'];
const lines: string[] = [];
if (depth === 0 && children.length > 1) {
lines.push('다음 조건 중 하나라도 만족하면 됩니다.');
}
children.forEach(child => {
lines.push(...describeNode(child, signalType, def, depth + 1));
});
return lines;
} }
if (node.type === 'NOT') { if (node.type === 'NOT') {
const child = node.children?.[0]; const child = node.children?.[0];
if (!child) return ['(비어 있는 NOT 그룹)']; if (!child) return [`${indent}(비어 있는 NOT 그룹)`];
const sub = describeNode(child, signalType, def, depth + 1); const sub = describeLogicTree(child, signalType, def, indent + ' ', false);
return ['다음 조건이 성립하지 않을 때:', ...sub.map(l => ` (부정) ${l.replace(/^•\s*/, '')}`)]; return [
`${indent}다음 조건이 성립하지 않을 때:`,
...sub.map(l => `${indent} (부정) ${l.replace(/^[\s•]+/, '')}`),
];
} }
return []; return [];
} }
function describeNode(
node: LogicNode,
signalType: 'buy' | 'sell',
def: DefType,
depth = 0,
): string[] {
const indent = depth > 0 ? ' '.repeat(depth) : '';
return describeLogicTree(node, signalType, def, indent, depth === 0);
}
function describeSignalBranches( function describeSignalBranches(
editorState: EditorConditionState | undefined, editorState: EditorConditionState | undefined,
fallbackRoot: LogicNode | null, fallbackRoot: LogicNode | null,
@@ -306,7 +406,7 @@ function describeSignalBranches(
? `${tf} 각 시간봉 마감 시점마다 아래 조건을 독립적으로 평가합니다.` ? `${tf} 각 시간봉 마감 시점마다 아래 조건을 독립적으로 평가합니다.`
: `${tf} 차트를 기준으로 아래 조건을 평가합니다.`, : `${tf} 차트를 기준으로 아래 조건을 평가합니다.`,
); );
bullets.push(...describeNode(root!, signalType, def)); bullets.push(...describeLogicTree(root!, signalType, def, '', true));
return { hasContent: true, paragraphs, bullets }; return { hasContent: true, paragraphs, bullets };
} }
@@ -321,9 +421,7 @@ function describeSignalBranches(
const tf = candleKo(branch.candleType); const tf = candleKo(branch.candleType);
bullets.push(`[시간봉 ${idx + 1}: ${tf}]`); bullets.push(`[시간봉 ${idx + 1}: ${tf}]`);
if (branch.root) { if (branch.root) {
bullets.push(...describeNode(branch.root, signalType, def, 1).map(l => bullets.push(...describeLogicTree(branch.root, signalType, def, ' ', false));
l.startsWith('•') ? ` ${l}` : `${l}`,
));
} }
}); });