가상투자 메뉴 기능 구현
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* 전략 DSL → 차트 IndicatorConfig[] 변환 (가상투자 차트 팝업)
|
||||
*/
|
||||
import type { StrategyDto } from './backendApi';
|
||||
import type { IndicatorConfig, Timeframe } from '../types';
|
||||
import type { LogicNode } from './strategyTypes';
|
||||
import {
|
||||
enrichIndicatorConfig,
|
||||
getIndicatorDef,
|
||||
getHLineLabel,
|
||||
type HLineDef,
|
||||
} from './indicatorRegistry';
|
||||
import { createDefaultSmaPlotVisibility, normalizeSmaConfig, smaPeriodKey } from './smaConfig';
|
||||
import { normalizeIchimokuConfig } from './ichimokuConfig';
|
||||
import { extractVirtualConditions } from './virtualStrategyConditions';
|
||||
|
||||
const DSL_TO_REGISTRY: Record<string, string> = {
|
||||
RSI: 'RSI',
|
||||
MACD: 'MACD',
|
||||
MA: 'SMA',
|
||||
EMA: 'EMA',
|
||||
BOLLINGER: 'BollingerBands',
|
||||
STOCHASTIC: 'Stochastic',
|
||||
WILLIAMS_R: 'WilliamsPercentRange',
|
||||
CCI: 'CCI',
|
||||
ADX: 'ADX',
|
||||
DMI: 'DMI',
|
||||
OBV: 'OBV',
|
||||
TRIX: 'TRIX',
|
||||
VOLUME_OSC: 'VolumeOscillator',
|
||||
VR: 'VR',
|
||||
DISPARITY: 'Disparity',
|
||||
PSYCHOLOGICAL: 'Psychological',
|
||||
NEW_PSYCHOLOGICAL: 'Psychological',
|
||||
INVEST_PSYCHOLOGICAL: 'InvestPsychological',
|
||||
BWI: 'BBBandWidth',
|
||||
DONCHIAN: 'DonchianChannels',
|
||||
ICHIMOKU: 'IchimokuCloud',
|
||||
ATR: 'ATR',
|
||||
MFI: 'MFI',
|
||||
};
|
||||
|
||||
interface IndicatorRef {
|
||||
dslType: string;
|
||||
registryType: string;
|
||||
period?: number;
|
||||
leftPeriod?: number;
|
||||
rightPeriod?: number;
|
||||
targetValue?: number | null;
|
||||
}
|
||||
|
||||
type ParamRecord = Record<string, number | string | boolean>;
|
||||
type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord;
|
||||
type GetVisual = (
|
||||
type: string,
|
||||
plots: import('./indicatorRegistry').PlotDef[],
|
||||
hlines: HLineDef[],
|
||||
) => {
|
||||
plots: import('./indicatorRegistry').PlotDef[];
|
||||
hlines: HLineDef[];
|
||||
cloudColors?: import('./ichimokuConfig').IchimokuCloudColors;
|
||||
};
|
||||
|
||||
let _idSeq = 0;
|
||||
function newIndId(): string {
|
||||
_idSeq += 1;
|
||||
return `vstrat_${_idSeq}_${Date.now()}`;
|
||||
}
|
||||
|
||||
export function candleTypeToTimeframe(candleType: string): Timeframe {
|
||||
const c = (candleType ?? '1m').trim().toLowerCase();
|
||||
if (c === '1d') return '1D';
|
||||
if (c === '1h') return '1h';
|
||||
if (c === '4h') return '4h';
|
||||
if (['1m', '3m', '5m', '15m', '30m'].includes(c)) return c as Timeframe;
|
||||
return '1m';
|
||||
}
|
||||
|
||||
export function resolveStrategyTimeframes(strategy: StrategyDto | undefined): Timeframe[] {
|
||||
if (!strategy) return ['1m'];
|
||||
const conditions = extractVirtualConditions(strategy);
|
||||
const set = new Set<Timeframe>();
|
||||
for (const row of conditions) {
|
||||
set.add(candleTypeToTimeframe(row.timeframe));
|
||||
}
|
||||
if (set.size === 0) set.add('1m');
|
||||
return [...set].sort((a, b) => {
|
||||
const order: Timeframe[] = ['1m', '3m', '5m', '15m', '30m', '1h', '4h', '1D', '1W', '1M'];
|
||||
return order.indexOf(a) - order.indexOf(b);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveStrategyPrimaryTimeframe(strategy: StrategyDto | undefined): Timeframe {
|
||||
const tfs = resolveStrategyTimeframes(strategy);
|
||||
return tfs[0] ?? '1m';
|
||||
}
|
||||
|
||||
function walkIndicatorRefs(
|
||||
node: LogicNode | null | undefined,
|
||||
out: IndicatorRef[],
|
||||
seen: Set<string>,
|
||||
): void {
|
||||
if (!node) return;
|
||||
|
||||
if (node.type === 'TIMEFRAME') {
|
||||
node.children?.forEach(c => walkIndicatorRefs(c, out, seen));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
const c = node.condition;
|
||||
const dslType = c.indicatorType;
|
||||
const registryType = DSL_TO_REGISTRY[dslType] ?? dslType;
|
||||
const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${c.targetValue ?? ''}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
out.push({
|
||||
dslType,
|
||||
registryType,
|
||||
period: c.period,
|
||||
leftPeriod: c.leftPeriod,
|
||||
rightPeriod: c.rightPeriod,
|
||||
targetValue: c.targetValue ?? c.compareValue ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
node.children?.forEach(ch => walkIndicatorRefs(ch, out, seen));
|
||||
}
|
||||
|
||||
function collectIndicatorRefs(strategy: StrategyDto): IndicatorRef[] {
|
||||
const out: IndicatorRef[] = [];
|
||||
const seen = new Set<string>();
|
||||
walkIndicatorRefs(strategy.buyCondition as LogicNode | null, out, seen);
|
||||
walkIndicatorRefs(strategy.sellCondition as LogicNode | null, out, seen);
|
||||
return out;
|
||||
}
|
||||
|
||||
function applyTargetHlines(
|
||||
hlines: HLineDef[],
|
||||
target: number | null | undefined,
|
||||
): HLineDef[] {
|
||||
if (target == null || Number.isNaN(target)) return hlines;
|
||||
const next = hlines.map(h => ({ ...h }));
|
||||
const prices = next.map(h => h.price);
|
||||
const idx = next.findIndex(h => Math.abs(h.price - target) < 1e-6);
|
||||
if (idx >= 0) {
|
||||
next[idx] = { ...next[idx], visible: true, price: target };
|
||||
return next;
|
||||
}
|
||||
next.push({
|
||||
price: target,
|
||||
color: '#ffb300',
|
||||
label: getHLineLabel(target, [...prices, target]),
|
||||
visible: true,
|
||||
lineStyle: 'dashed',
|
||||
lineWidth: 1,
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildOneIndicator(
|
||||
ref: IndicatorRef,
|
||||
getParams: GetParams,
|
||||
getVisual: GetVisual,
|
||||
): IndicatorConfig | null {
|
||||
const def = getIndicatorDef(ref.registryType);
|
||||
if (!def) return null;
|
||||
|
||||
const params = getParams(ref.registryType, def.defaultParams as ParamRecord);
|
||||
const { plots, hlines, cloudColors } = getVisual(ref.registryType, def.plots, def.hlines ?? []);
|
||||
|
||||
let cfg: IndicatorConfig = {
|
||||
id: newIndId(),
|
||||
type: def.type,
|
||||
params: { ...params },
|
||||
plots,
|
||||
hlines,
|
||||
...(cloudColors ? { cloudColors } : {}),
|
||||
};
|
||||
|
||||
if (ref.registryType === 'SMA') {
|
||||
const periods = [ref.period, ref.leftPeriod, ref.rightPeriod]
|
||||
.filter((p): p is number => typeof p === 'number' && p > 0);
|
||||
if (periods.length > 0) {
|
||||
const p = { ...cfg.params };
|
||||
periods.slice(0, 11).forEach((val, i) => {
|
||||
p[smaPeriodKey(i + 1)] = val;
|
||||
});
|
||||
cfg = { ...cfg, params: p };
|
||||
}
|
||||
cfg = normalizeSmaConfig({
|
||||
...cfg,
|
||||
plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(),
|
||||
});
|
||||
} else if (ref.period != null && ref.period > 0) {
|
||||
cfg = {
|
||||
...cfg,
|
||||
params: { ...cfg.params, length: ref.period },
|
||||
};
|
||||
}
|
||||
|
||||
if (ref.registryType === 'IchimokuCloud') {
|
||||
cfg = normalizeIchimokuConfig(cfg);
|
||||
}
|
||||
|
||||
if (ref.targetValue != null) {
|
||||
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
|
||||
}
|
||||
|
||||
return enrichIndicatorConfig(cfg);
|
||||
}
|
||||
|
||||
/** 전략에 포함된 보조지표를 차트용 IndicatorConfig 로 변환 (중복 type 제거) */
|
||||
export function buildStrategyChartIndicators(
|
||||
strategy: StrategyDto | undefined,
|
||||
getParams: GetParams,
|
||||
getVisual: GetVisual,
|
||||
): IndicatorConfig[] {
|
||||
if (!strategy) return [];
|
||||
|
||||
const refs = collectIndicatorRefs(strategy);
|
||||
const byType = new Map<string, IndicatorRef>();
|
||||
|
||||
for (const ref of refs) {
|
||||
const existing = byType.get(ref.registryType);
|
||||
if (!existing) {
|
||||
byType.set(ref.registryType, ref);
|
||||
continue;
|
||||
}
|
||||
byType.set(ref.registryType, {
|
||||
...existing,
|
||||
period: existing.period ?? ref.period,
|
||||
leftPeriod: existing.leftPeriod ?? ref.leftPeriod,
|
||||
rightPeriod: existing.rightPeriod ?? ref.rightPeriod,
|
||||
targetValue: existing.targetValue ?? ref.targetValue,
|
||||
});
|
||||
}
|
||||
|
||||
const result: IndicatorConfig[] = [];
|
||||
for (const ref of byType.values()) {
|
||||
const cfg = buildOneIndicator(ref, getParams, getVisual);
|
||||
if (cfg) result.push(cfg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user