보조지표 직접입력 오류 수정

This commit is contained in:
Macbook
2026-06-17 23:28:17 +09:00
parent 5236a6cd46
commit 3130de113b
5 changed files with 68 additions and 23 deletions
@@ -20,12 +20,18 @@ public final class IndicatorHlineResolver {
if (field == null || field.isBlank()) return null;
// K_ 직접입력 — targetValue 가 있으면 thresholdOverride 플래그와 무관하게 우선
if (field.startsWith("K_") && cond != null && cond.has("targetValue") && !cond.path("targetValue").isNull()) {
return cond.path("targetValue").asDouble();
boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false);
// K_ 직접입력 — targetValue·override 시 필드명/스냅샷 숫자 우선
if (field.startsWith("K_")) {
if (cond != null && cond.has("targetValue") && !cond.path("targetValue").isNull()) {
return cond.path("targetValue").asDouble();
}
if (override) {
try { return Double.parseDouble(field.substring(2)); } catch (NumberFormatException ignored) {}
}
}
boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false);
if (override) {
if (cond.has("targetValue") && !cond.path("targetValue").isNull()) {
return cond.path("targetValue").asDouble();
@@ -281,6 +281,19 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
return () => { cancelled = true; };
}, [selectedStrategyId]);
/** 편집기 저장 후 동일 전략 평가 시 DSL·시그널 갱신 */
useEffect(() => {
const onSaved = (e: Event) => {
const detail = (e as CustomEvent<{ strategyId?: number }>).detail;
if (detail?.strategyId == null || detail.strategyId !== selectedStrategyId) return;
void loadStrategy(detail.strategyId).then(s => {
setSelectedStrategy(s ? hydrateStrategyDto(s) : null);
});
};
window.addEventListener('gc:strategy-saved', onSaved);
return () => window.removeEventListener('gc:strategy-saved', onSaved);
}, [selectedStrategyId]);
const filteredStrategies = useMemo(() => {
const q = strategySearch.trim().toLowerCase();
if (!q) return strategies;
+24 -16
View File
@@ -82,6 +82,15 @@ function parseFieldPeriod(field?: string): number | null {
return Number.isFinite(n) && n > 0 ? n : null;
}
/** 조건 DSL에서 Ta4j 평가용 기간 — period 필드·leftField 접미사 */
export function resolveConditionDslPeriod(c: ConditionDSL): number | undefined {
if (c.period != null && c.period > 0) return c.period;
const fromLeft = parseFieldPeriod(c.leftField);
if (fromLeft) return fromLeft;
if (c.composite && c.leftPeriod != null && c.leftPeriod > 0) return c.leftPeriod;
return undefined;
}
/** 조건 노드의 RSI 등 값(좌측) 기간 — true=전략 전용, false/미설정=보조지표 설정(DEF) 상속 */
export function isValuePeriodOverridden(cond: ConditionDSL): boolean {
return cond.valuePeriodOverride === true;
@@ -271,7 +280,7 @@ export function activateDirectThresholdInput(
?? getChartReferenceThreshold(cond, side, inheritedField, def)
?? parseThresholdField(side === 'right' ? cond.rightField : cond.leftField);
if (value == null || !Number.isFinite(value)) return cond;
return setConditionThreshold(cond, side, value, def, { directInput: true });
return setConditionThreshold(cond, side, value, def);
}
export function setConditionThreshold(
@@ -302,22 +311,21 @@ export function setConditionThreshold(
? getChartReferenceThreshold(cond, side, inheritedField, def)
: null;
if (!options?.directInput) {
const matchedRole = def
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
: null;
if (matchedRole != null) {
const { targetValue: _t, ...rest } = cond;
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
}
// 30/50/70 등 hline 역할과 정확히 일치하면 직접입력·프리셋 모두 HL_* 로 통일 (평가 일치)
const matchedRole = def
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
: null;
if (matchedRole != null) {
const { targetValue: _t, ...rest } = cond;
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
}
if (chartRef != null && Math.abs(chartRef - value) < 0.0001) {
const role = isThresholdSymbol(inheritedField)
? inheritedField
: (side === 'right' ? THRESHOLD_HL_OVER : inheritedField);
const { targetValue: _t, ...rest } = cond;
return { ...rest, [fieldKey]: role, thresholdOverride: false };
}
if (!options?.directInput && chartRef != null && Math.abs(chartRef - value) < 0.0001) {
const role = isThresholdSymbol(inheritedField)
? inheritedField
: (side === 'right' ? THRESHOLD_HL_OVER : inheritedField);
const { targetValue: _t, ...rest } = cond;
return { ...rest, [fieldKey]: role, thresholdOverride: false };
}
return {
@@ -11,6 +11,7 @@ import {
} from './conditionPeriods';
import {
defaultInheritedThresholdField,
inferThresholdRoleForValue,
inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
isThresholdFieldOrSymbol,
@@ -97,10 +98,26 @@ export function migrateLogicRootForEditor(
return repairPriceExtremePairForest(migrated.root, migrated.orphans);
}
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거 (직접입력 K_+targetValue 는 유지) */
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거; 직접입력도 hline 역할과 일치하면 HL_* 로 통일 */
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
let next: ConditionDSL = { ...cond };
if (isThresholdOverridden(next)) {
if (next.targetValue != null && Number.isFinite(next.targetValue)) {
const role = inferThresholdRoleForValue(next.targetValue, next.indicatorType, {});
if (role != null) {
const { targetValue: _t, ...rest } = next;
next = { ...rest, rightField: role, thresholdOverride: false };
}
} else if (next.rightField?.startsWith('K_')) {
const role = inferThresholdSymbolFromLegacyExact(next.rightField, next.indicatorType, {});
if (role != null) {
const { targetValue: _t, ...rest } = next;
next = { ...rest, rightField: role, thresholdOverride: false };
}
}
}
if (!isThresholdOverridden(next)) {
if (next.rightField?.startsWith('K_')) {
next.rightField = inferThresholdSymbolFromLegacyExact(
@@ -18,6 +18,7 @@ import { normalizeSmaConfig } from './smaConfig';
import { normalizeIchimokuConfig } from './ichimokuConfig';
import type { EditorConditionState } from './strategyConditionSerde';
import { collectUiEvaluationTimeframes } from './strategyTimeframeSync';
import { resolveConditionDslPeriod } from './conditionPeriods';
import {
applyStrategyRefToParams,
resolveConditionThresholdForChart,
@@ -139,13 +140,13 @@ 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 ?? ''}:${resolveConditionThresholdForChart(c) ?? ''}`;
const key = `${registryType}:${resolveConditionDslPeriod(c) ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${resolveConditionThresholdForChart(c) ?? ''}`;
if (seen.has(key)) return;
seen.add(key);
out.push({
dslType,
registryType,
period: c.period,
period: resolveConditionDslPeriod(c),
leftPeriod: c.leftPeriod,
rightPeriod: c.rightPeriod,
targetValue: resolveConditionThresholdForChart(c),