114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
/**
|
|
* 전략 DSL → 가상투자 이퀄라이저 표시용 조건 추출
|
|
*/
|
|
import type { LogicNode } from './strategyTypes';
|
|
import { CONDITION_LABEL } from './strategyTypes';
|
|
import type { StrategyDto } from './backendApi';
|
|
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
|
|
import { coerceFiniteNumber, safeToFixed } from './safeFormat';
|
|
import { asLogicNode } from './strategyHydrate';
|
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
|
|
|
export { coerceFiniteNumber } from './safeFormat';
|
|
|
|
export interface VirtualConditionRow {
|
|
id: string;
|
|
indicatorType: string;
|
|
displayName: string;
|
|
conditionType: string;
|
|
conditionLabel: string;
|
|
targetValue: number | null;
|
|
timeframe: string;
|
|
side: 'buy' | 'sell';
|
|
/** 지표 계산 plot id (RSI, MACD 등) */
|
|
plotKey: string;
|
|
/** 백엔드 Ta4j Rule.isSatisfied 결과 (있으면 우선) */
|
|
satisfied?: boolean | null;
|
|
/** 백엔드 임계값 라벨 (MA cross 등) */
|
|
thresholdLabel?: string;
|
|
}
|
|
|
|
function walk(
|
|
node: LogicNode | null | undefined,
|
|
timeframe: string,
|
|
side: 'buy' | 'sell',
|
|
out: VirtualConditionRow[],
|
|
seen: Set<string>,
|
|
): void {
|
|
if (!node) return;
|
|
|
|
if (node.type === 'TIMEFRAME') {
|
|
const tf = normalizeStartCandleType(
|
|
node.candleTypes?.[0] ?? node.candleType ?? timeframe,
|
|
);
|
|
node.children?.forEach(c => walk(c, tf, side, out, seen));
|
|
return;
|
|
}
|
|
|
|
if (node.type === 'NOT') {
|
|
const notChild = node.children?.[0] ?? (node as LogicNode & { child?: LogicNode }).child;
|
|
if (notChild) walk(notChild, timeframe, side, out, seen);
|
|
return;
|
|
}
|
|
|
|
if (node.type === 'AND' || node.type === 'OR') {
|
|
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
|
return;
|
|
}
|
|
|
|
// StrategyDslToTa4jAdapter 와 동일: type 미설정(구버전 DSL) 시 CONDITION 으로 처리
|
|
if ((!node.type || node.type === 'CONDITION') && node.condition) {
|
|
const c = node.condition;
|
|
const rowTf = normalizeStartCandleType(c.leftCandleType ?? c.rightCandleType ?? timeframe);
|
|
const rowId = node.id ? `${node.id}-${side}` : `${side}:${rowTf}:${c.indicatorType}:${out.length}`;
|
|
if (seen.has(rowId)) return;
|
|
seen.add(rowId);
|
|
|
|
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
|
|
const target = coerceFiniteNumber(c.targetValue ?? c.compareValue ?? null);
|
|
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
|
|
|
|
out.push({
|
|
id: rowId,
|
|
indicatorType: c.indicatorType,
|
|
displayName: formatIndicatorDisplayLabel(c.indicatorType),
|
|
conditionType: c.conditionType,
|
|
conditionLabel: condLabel,
|
|
targetValue: target,
|
|
timeframe: rowTf,
|
|
side,
|
|
plotKey,
|
|
});
|
|
return;
|
|
}
|
|
|
|
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
|
}
|
|
|
|
/** 목록 표시용 — 동일 지표 복수 조건 구분 */
|
|
export function formatVirtualConditionListLabel(
|
|
row: Pick<VirtualConditionRow, 'displayName' | 'thresholdLabel' | 'conditionLabel' | 'timeframe'>,
|
|
): string {
|
|
const parts = [row.displayName];
|
|
if (row.thresholdLabel) parts.push(row.thresholdLabel);
|
|
else if (row.conditionLabel) parts.push(row.conditionLabel);
|
|
if (row.timeframe && row.timeframe !== '1m') parts.push(row.timeframe);
|
|
return parts.join(' · ');
|
|
}
|
|
|
|
export function extractVirtualConditions(strategy: StrategyDto | null | undefined): VirtualConditionRow[] {
|
|
if (!strategy) return [];
|
|
const out: VirtualConditionRow[] = [];
|
|
const seen = new Set<string>();
|
|
walk(asLogicNode(strategy.buyCondition), '1m', 'buy', out, seen);
|
|
walk(asLogicNode(strategy.sellCondition), '1m', 'sell', out, seen);
|
|
return out;
|
|
}
|
|
|
|
export function formatIndicatorValue(v: number | null | unknown): string {
|
|
const n = coerceFiniteNumber(v);
|
|
if (n == null) return '—';
|
|
if (Math.abs(n) >= 1000) return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
|
return safeToFixed(n, 2);
|
|
}
|