보조지표 직접입력 오류 수정
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user