보조지표 전략체크 수정

This commit is contained in:
Macbook
2026-06-17 21:49:15 +09:00
parent 1698dd9c8e
commit 3c269db6b7
7 changed files with 300 additions and 40 deletions
@@ -2,7 +2,8 @@
* 전략 평가 — 전략에 포함된 지표 파라미터 맵
*/
import type { StrategyDto } from './backendApi';
import { collectStrategyRegistryTypes } from './strategyToChartIndicators';
import { collectIndicatorRefs } from './strategyToChartIndicators';
import { applyStrategyRefToParams } from './strategyIndicatorSync';
export type EvalIndicatorParams = Record<string, Record<string, number | string | boolean>>;
@@ -10,10 +11,13 @@ export function buildEvalParamsFromStrategy(
strategy: StrategyDto | null | undefined,
getParams: (type: string) => Record<string, number | string | boolean>,
): EvalIndicatorParams {
const types = collectStrategyRegistryTypes(strategy);
if (!strategy) return {};
const out: EvalIndicatorParams = {};
for (const type of types) {
out[type] = { ...getParams(type) };
for (const ref of collectIndicatorRefs(strategy)) {
const base = { ...getParams(ref.registryType) };
const merged = applyStrategyRefToParams(ref, base);
out[ref.registryType] = { ...(out[ref.registryType] ?? {}), ...merged };
}
return out;
}
@@ -0,0 +1,96 @@
/**
* 전략 DSL 기간·임계값 → 차트 지표 params / hline 동기화
* (Ta4j StrategyDslToTa4jAdapter.effectivePeriod 키와 동일)
*/
import type { ConditionDSL } from './strategyTypes';
import { isThresholdSymbol } from './thresholdSymbols';
export interface StrategyIndicatorRef {
dslType: string;
registryType: string;
period?: number;
leftPeriod?: number;
rightPeriod?: number;
targetValue?: number | null;
}
/** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */
export function resolveConditionThresholdForChart(c: ConditionDSL): number | null {
if (c.thresholdOverride === false) return null;
if (c.targetValue != null && Number.isFinite(c.targetValue)) return c.targetValue;
if (c.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue;
const rf = c.rightField ?? '';
if (rf.startsWith('K_')) {
const n = parseFloat(rf.slice(2));
return Number.isFinite(n) ? n : null;
}
if (isThresholdSymbol(rf)) return null;
return null;
}
const REGISTRY_PRIMARY_PERIOD_KEY: Record<string, string> = {
RSI: 'length',
CCI: 'length',
WilliamsPercentRange: 'length',
Stochastic: 'kLength',
MACD: 'fastLength',
ADX: 'adxSmoothing',
DMI: 'diLength',
TRIX: 'length',
BollingerBands: 'length',
DonchianChannels: 'length',
Envelope: 'length',
Disparity: 'length',
Psychological: 'length',
NewPsychological: 'length',
InvestPsychological: 'length',
VR: 'length',
VolumeOscillator: 'shortLength',
MFI: 'length',
ATR: 'length',
ChandeMO: 'length',
DPO: 'length',
RVI: 'length',
BBPercentB: 'length',
BBBandWidth: 'length',
StochRSI: 'lengthRSI',
};
function setIfPositive(params: Record<string, number | string | boolean>, key: string, value?: number) {
if (value != null && value > 0) params[key] = value;
}
/** 전략 ref 기간을 지표 params에 덮어씀 (DB 기본값보다 우선) */
export function applyStrategyRefToParams(
ref: StrategyIndicatorRef,
params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
const next = { ...params };
const primaryKey = REGISTRY_PRIMARY_PERIOD_KEY[ref.registryType];
if (primaryKey && ref.period != null && ref.period > 0) {
next[primaryKey] = ref.period;
}
switch (ref.registryType) {
case 'SMA':
case 'EMA':
if (ref.period != null && ref.period > 0) next.length = ref.period;
setIfPositive(next, 'fastLength', ref.leftPeriod);
setIfPositive(next, 'slowLength', ref.rightPeriod);
break;
case 'MACross':
setIfPositive(next, 'fastLength', ref.leftPeriod ?? ref.period);
setIfPositive(next, 'slowLength', ref.rightPeriod);
break;
case 'Stochastic':
setIfPositive(next, 'dSmoothing', ref.rightPeriod);
break;
case 'StochRSI':
setIfPositive(next, 'lengthStoch', ref.rightPeriod);
break;
default:
break;
}
return next;
}
+10 -10
View File
@@ -1,7 +1,7 @@
import type { OHLCVBar } from '../types';
import type { StrategyDto } from './backendApi';
import { extractVirtualConditions } from './virtualStrategyConditions';
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
import { collectIndicatorRefs } from './strategyToChartIndicators';
const DSL_TO_REGISTRY: Record<string, string> = {
RSI: 'RSI',
@@ -132,23 +132,23 @@ function refLinesFor(registryType: string): number[] | undefined {
export function collectStrategyOscillatorPanes(strategy: StrategyDto | undefined): OscillatorPaneSpec[] {
if (!strategy) return [];
const rows = extractVirtualConditions(strategy);
const seen = new Set<string>();
const out: OscillatorPaneSpec[] = [];
for (const row of rows) {
if (OVERLAY_DSL.has(row.indicatorType)) continue;
const registryType = DSL_TO_REGISTRY[row.indicatorType] ?? row.indicatorType;
for (const ref of collectIndicatorRefs(strategy)) {
if (OVERLAY_DSL.has(ref.dslType)) continue;
const registryType = DSL_TO_REGISTRY[ref.dslType] ?? ref.registryType;
if (OVERLAY_DSL.has(registryType) || registryType === 'SMA' || registryType === 'EMA'
|| registryType === 'BollingerBands' || registryType === 'IchimokuCloud') {
continue;
}
if (seen.has(registryType)) continue;
seen.add(registryType);
const key = `${registryType}:${ref.period ?? ''}`;
if (seen.has(key)) continue;
seen.add(key);
out.push({
key: registryType,
key,
registryType,
label: formatIndicatorDisplayLabel(row.indicatorType),
period: undefined,
label: formatIndicatorDisplayLabel(ref.dslType),
period: ref.period,
});
}
return out;
+33 -25
View File
@@ -18,6 +18,11 @@ import { normalizeSmaConfig } from './smaConfig';
import { normalizeIchimokuConfig } from './ichimokuConfig';
import type { EditorConditionState } from './strategyConditionSerde';
import { collectUiEvaluationTimeframes } from './strategyTimeframeSync';
import {
applyStrategyRefToParams,
resolveConditionThresholdForChart,
type StrategyIndicatorRef,
} from './strategyIndicatorSync';
const DSL_TO_REGISTRY: Record<string, string> = {
RSI: 'RSI',
@@ -47,14 +52,7 @@ const DSL_TO_REGISTRY: Record<string, string> = {
MFI: 'MFI',
};
interface IndicatorRef {
dslType: string;
registryType: string;
period?: number;
leftPeriod?: number;
rightPeriod?: number;
targetValue?: number | null;
}
interface IndicatorRef extends StrategyIndicatorRef {}
type ParamRecord = Record<string, number | string | boolean>;
type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord;
@@ -141,7 +139,7 @@ function walkIndicatorRefs(
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 ?? ''}`;
const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${resolveConditionThresholdForChart(c) ?? ''}`;
if (seen.has(key)) return;
seen.add(key);
out.push({
@@ -150,7 +148,7 @@ function walkIndicatorRefs(
period: c.period,
leftPeriod: c.leftPeriod,
rightPeriod: c.rightPeriod,
targetValue: c.targetValue ?? c.compareValue ?? null,
targetValue: resolveConditionThresholdForChart(c),
});
return;
}
@@ -158,7 +156,7 @@ function walkIndicatorRefs(
node.children?.forEach(ch => walkIndicatorRefs(ch, out, seen));
}
function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] {
export function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] {
const out: IndicatorRef[] = [];
const seen = new Set<string>();
if (!side || side === 'BUY') {
@@ -215,6 +213,11 @@ function buildOneIndicator(
);
if (!cfg) return null;
cfg = {
...cfg,
params: applyStrategyRefToParams(ref, cfg.params),
};
if (ref.targetValue != null) {
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
}
@@ -237,25 +240,17 @@ export function buildStrategyChartIndicators(
if (!strategy) return [];
const refs = collectIndicatorRefs(strategy, side);
const byType = new Map<string, IndicatorRef>();
const byKey = new Map<string, IndicatorRef>();
for (const ref of refs) {
const existing = byType.get(ref.registryType);
if (!existing) {
byType.set(ref.registryType, ref);
continue;
const key = stableStrategyIndicatorId(ref);
if (!byKey.has(key)) {
byKey.set(key, ref);
}
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()) {
for (const ref of byKey.values()) {
const cfg = buildOneIndicator(ref, getParams, getVisual);
if (cfg) result.push(cfg);
}
@@ -384,14 +379,27 @@ export function buildStrategyEvaluationChartIndicators(
if (!strategy) return [];
const strategyTypes = new Set(collectStrategyRegistryTypes(strategy));
const refById = new Map(
collectIndicatorRefs(strategy).map(ref => [stableStrategyIndicatorId(ref), ref]),
);
let inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisual);
inds = ensurePaperChartOverlays(inds, getParams, getVisual);
return inds.map(ind => {
let cfg = applyDbVisualSettingsForStrategyChart(ind, getVisual);
const ref = refById.get(ind.id);
if (ref) {
cfg = {
...cfg,
params: applyStrategyRefToParams(ref, cfg.params),
};
if (ref.targetValue != null) {
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
}
}
if (strategyTypes.has(cfg.type)) {
cfg = { ...cfg, hidden: false };
}
return cfg;
return enrichIndicatorConfig(cfg);
});
}