보조지표 전략체크 수정
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user