213 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|