가상투자 메뉴 기능 구현
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 전략 DSL → 가상투자 이퀄라이저 표시용 조건 추출
|
||||
*/
|
||||
import type { LogicNode } from './strategyTypes';
|
||||
import { CONDITION_LABEL } from './strategyTypes';
|
||||
import type { StrategyDto } from './backendApi';
|
||||
import { getIndicatorDef } from './indicatorRegistry';
|
||||
|
||||
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 = node.candleType ?? timeframe;
|
||||
node.children?.forEach(c => walk(c, tf, side, out, seen));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
const c = node.condition;
|
||||
const key = `${side}:${timeframe}:${c.indicatorType}:${c.conditionType}:${c.targetValue ?? ''}:${c.leftField ?? ''}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
|
||||
const def = getIndicatorDef(c.indicatorType);
|
||||
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
|
||||
const target = c.targetValue ?? c.compareValue ?? null;
|
||||
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
|
||||
|
||||
out.push({
|
||||
id: `${node.id}-${side}`,
|
||||
indicatorType: c.indicatorType,
|
||||
displayName: def?.koreanName ?? def?.shortName ?? c.indicatorType,
|
||||
conditionType: c.conditionType,
|
||||
conditionLabel: condLabel,
|
||||
targetValue: target,
|
||||
timeframe,
|
||||
side,
|
||||
plotKey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
||||
}
|
||||
|
||||
export function extractVirtualConditions(strategy: StrategyDto | null | undefined): VirtualConditionRow[] {
|
||||
if (!strategy) return [];
|
||||
const out: VirtualConditionRow[] = [];
|
||||
const seen = new Set<string>();
|
||||
walk(strategy.buyCondition as LogicNode | null, '1m', 'buy', out, seen);
|
||||
walk(strategy.sellCondition as LogicNode | null, '1m', 'sell', out, seen);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 조건 충족 여부 (단순 임계값 비교) */
|
||||
export function isConditionMet(
|
||||
conditionType: string,
|
||||
current: number | null,
|
||||
target: number | null,
|
||||
): boolean | null {
|
||||
if (current == null || target == null) return null;
|
||||
switch (conditionType) {
|
||||
case 'GT': case 'CROSS_UP': return current > target;
|
||||
case 'LT': case 'CROSS_DOWN': return current < target;
|
||||
case 'GTE': return current >= target;
|
||||
case 'LTE': return current <= target;
|
||||
case 'EQ': return Math.abs(current - target) < 1e-6;
|
||||
case 'NEQ': return Math.abs(current - target) >= 1e-6;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatIndicatorValue(v: number | null): string {
|
||||
if (v == null || Number.isNaN(v)) return '—';
|
||||
if (Math.abs(v) >= 1000) return v.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
||||
return v.toFixed(2);
|
||||
}
|
||||
Reference in New Issue
Block a user