Files
goldenChart/frontend/src/components/strategyEditor/LogicExpressionPreview.tsx
T
2026-05-27 23:36:48 +09:00

213 lines
6.7 KiB
TypeScript

import React from 'react';
import type { LogicNode } from '../../utils/strategyTypes';
import { nodeToText, type DefType } from '../../utils/strategyEditorShared';
import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage';
import { getIndicatorPaletteCategory } from './paletteCategories';
import {
collectEditorBranches,
normalizeStartCombineOp,
type EditorConditionState,
} from '../../utils/strategyConditionSerde';
import { formatStartCandleLabel } from '../../utils/strategyStartNodes';
interface Props {
buyCondition: LogicNode | null;
sellCondition: LogicNode | null;
buyEditorState?: EditorConditionState;
sellEditorState?: EditorConditionState;
orphanCount: number;
def: DefType;
}
function renderCondition(node: LogicNode, def: DefType): React.ReactNode {
const full = nodeToText(node, def);
const ind = node.condition?.indicatorType ?? '';
const indLabel = getStrategyIndicatorDisplayName(ind);
const prefix = `${indLabel} -`;
const tail = indLabel && full.startsWith(prefix) ? full.slice(prefix.length) : full;
const cat = getIndicatorPaletteCategory(ind);
return (
<>
<span className="se-formula-punc">(</span>
{indLabel ? (
<>
<span className={`se-formula-ind se-formula-ind--${cat}`}>{indLabel}</span>
{tail ? <span className="se-formula-detail">{tail.startsWith(' ') ? tail : ` ${tail}`}</span> : null}
</>
) : (
<span className="se-formula-detail">{full || '?'}</span>
)}
<span className="se-formula-punc">)</span>
</>
);
}
function renderNode(node: LogicNode, def: DefType): React.ReactNode {
if (node.type === 'CONDITION') return renderCondition(node, def);
if (node.type === 'AND') {
const children = node.children ?? [];
if (children.length === 0) return <span className="se-formula-empty">( AND)</span>;
return (
<>
<span className="se-formula-punc">(</span>
{children.map((child, i) => (
<React.Fragment key={child.id}>
{i > 0 ? <span className="se-formula-logic se-formula-logic--and"> AND </span> : null}
{renderNode(child, def)}
</React.Fragment>
))}
<span className="se-formula-punc">)</span>
</>
);
}
if (node.type === 'OR') {
const children = node.children ?? [];
if (children.length === 0) return <span className="se-formula-empty">( OR)</span>;
return (
<>
<span className="se-formula-punc">(</span>
{children.map((child, i) => (
<React.Fragment key={child.id}>
{i > 0 ? <span className="se-formula-logic se-formula-logic--or"> OR </span> : null}
{renderNode(child, def)}
</React.Fragment>
))}
<span className="se-formula-punc">)</span>
</>
);
}
if (node.type === 'NOT') {
const child = node.children?.[0];
return child ? (
<>
<span className="se-formula-logic se-formula-logic--not">NOT </span>
{renderNode(child, def)}
</>
) : (
<span className="se-formula-empty">( NOT)</span>
);
}
if (node.type === 'TIMEFRAME') {
const inner = node.children?.[0];
const types = node.candleTypes?.length
? node.candleTypes
: [node.candleType ?? '1m'];
const tf = types.length > 1
? types.map(formatStartCandleLabel).join(' · ')
: formatStartCandleLabel(types[0]);
return inner ? (
<>
<span className="se-formula-tf">[{tf}]</span>
{' '}
{renderNode(inner, def)}
</>
) : (
<span className="se-formula-empty">[{tf} 조건]</span>
);
}
return null;
}
function renderSignalBranches(
editorState: EditorConditionState | undefined,
fallbackRoot: LogicNode | null,
def: DefType,
): React.ReactNode {
const branches = editorState
? collectEditorBranches(editorState)
: fallbackRoot
? [{ candleType: '1m', root: fallbackRoot }]
: [];
if (branches.length === 0) {
return <span className="se-formula-empty">(연결된 조건 없음)</span>;
}
if (branches.length === 1) {
const { candleType, candleTypes, root } = branches[0];
if (!root) return <span className="se-formula-empty">(연결된 조건 없음)</span>;
const tfLabel = candleTypes && candleTypes.length > 1
? candleTypes.map(formatStartCandleLabel).join(' · ')
: formatStartCandleLabel(candleType);
return (
<>
<span className="se-formula-tf">[{tfLabel}]</span>
{' '}
{renderNode(root, def)}
</>
);
}
const combineOp = normalizeStartCombineOp(editorState?.startCombineOp);
const combineClass = combineOp === 'AND' ? 'se-formula-logic--and' : 'se-formula-logic--or';
return (
<>
<span className="se-formula-punc">(</span>
{branches.map((branch, i) => (
<React.Fragment key={`${branch.candleType}-${i}`}>
{i > 0 ? (
<span className={`se-formula-logic ${combineClass}`}> {combineOp} </span>
) : null}
{branch.root ? (
<>
<span className="se-formula-tf">[{formatStartCandleLabel(branch.candleType)}]</span>
{' '}
{renderNode(branch.root, def)}
</>
) : (
<span className="se-formula-empty">[{formatStartCandleLabel(branch.candleType)} 조건]</span>
)}
</React.Fragment>
))}
<span className="se-formula-punc">)</span>
</>
);
}
export default function LogicExpressionPreview({
buyCondition,
sellCondition,
buyEditorState,
sellEditorState,
orphanCount,
def,
}: Props) {
const hasBody = buyCondition || sellCondition
|| (buyEditorState && collectEditorBranches(buyEditorState).some(b => b.root))
|| (sellEditorState && collectEditorBranches(sellEditorState).some(b => b.root));
return (
<div className="se-terminal-text se-terminal-text--rich">
{!hasBody && (
<span className="se-formula-empty">(연결된 조건을 구성하세요)</span>
)}
{(buyCondition || buyEditorState) && (
<div className="se-formula-line">
<span className="se-formula-signal se-formula-signal--buy">[매수]</span>
{' '}
{renderSignalBranches(buyEditorState, buyCondition, def)}
</div>
)}
{(sellCondition || sellEditorState) && (
<div className="se-formula-line">
<span className="se-formula-signal se-formula-signal--sell">[매도]</span>
{' '}
{renderSignalBranches(sellEditorState, sellCondition, def)}
</div>
)}
{orphanCount > 0 && (
<div className="se-formula-line se-formula-comment">
{'// 미연결 요소 '}{orphanCount}{'개 — 전략 코드 미포함'}
</div>
)}
</div>
);
}