평가로직 오류 수정
This commit is contained in:
@@ -5,6 +5,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import { asLogicNode } from '../../utils/strategyHydrate';
|
||||
import { decodeConditionForEditor } from '../../utils/strategyConditionSerde';
|
||||
import { normalizeLogicRootForPersistence } from '../../utils/strategyConditionNormalize';
|
||||
import { buildDefFromGetParams } from '../../utils/strategyEditorShared';
|
||||
import LogicExpressionPreview from '../strategyEditor/LogicExpressionPreview';
|
||||
import LogicExpressionNarrative from '../strategyEditor/LogicExpressionNarrative';
|
||||
@@ -34,11 +35,11 @@ const StrategyEvaluationConditionPanel: React.FC<Props> = ({
|
||||
const def = useMemo(() => buildDefFromGetParams(resolveGetParams), [resolveGetParams]);
|
||||
|
||||
const buyCondition = useMemo(
|
||||
() => (strategy ? asLogicNode(strategy.buyCondition) : null),
|
||||
() => normalizeLogicRootForPersistence(strategy ? asLogicNode(strategy.buyCondition) : null),
|
||||
[strategy?.buyCondition],
|
||||
);
|
||||
const sellCondition = useMemo(
|
||||
() => (strategy ? asLogicNode(strategy.sellCondition) : null),
|
||||
() => normalizeLogicRootForPersistence(strategy ? asLogicNode(strategy.sellCondition) : null),
|
||||
[strategy?.sellCondition],
|
||||
);
|
||||
|
||||
|
||||
@@ -21,14 +21,28 @@ import {
|
||||
} from './thresholdSymbols';
|
||||
import { repairPriceExtremePairForest } from './priceExtremeBreakoutPair';
|
||||
|
||||
function coerceTargetValue(raw: unknown): number | null {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null;
|
||||
if (typeof raw === 'string') {
|
||||
const n = parseFloat(raw.trim());
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isConditionCarrier(node: LogicNode): node is LogicNode & { condition: ConditionDSL } {
|
||||
return !!node.condition && (node.type === 'CONDITION' || node.type == null || node.type === undefined);
|
||||
}
|
||||
|
||||
function migrateConditionThreshold(
|
||||
cond: ConditionDSL,
|
||||
def: DefType,
|
||||
signalType: 'buy' | 'sell',
|
||||
): ConditionDSL {
|
||||
if (isThresholdOverridden(cond)) {
|
||||
const val = cond.targetValue ?? parseThresholdField(cond.rightField);
|
||||
if (val != null && Number.isFinite(val)) {
|
||||
const val = coerceTargetValue(cond.targetValue) ?? parseThresholdField(cond.rightField);
|
||||
if (val != null) {
|
||||
const role = inferThresholdRoleForValue(val, cond.indicatorType, def.hlThresh);
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
@@ -92,12 +106,20 @@ function migrateLogicNode(
|
||||
signalType: 'buy' | 'sell',
|
||||
): LogicNode {
|
||||
let next = node;
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
next = { ...node, condition: migrateConditionForEditor(node.condition, def, signalType) };
|
||||
if (isConditionCarrier(node)) {
|
||||
next = {
|
||||
...node,
|
||||
type: node.type ?? 'CONDITION',
|
||||
condition: migrateConditionForEditor(node.condition, def, signalType),
|
||||
};
|
||||
}
|
||||
if (next.children?.length) {
|
||||
next = { ...next, children: next.children.map(c => migrateLogicNode(c, def, signalType)) };
|
||||
}
|
||||
const legacyChild = (next as LogicNode & { child?: LogicNode }).child;
|
||||
if (legacyChild) {
|
||||
next = { ...next, child: migrateLogicNode(legacyChild, def, signalType) } as LogicNode;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -115,13 +137,17 @@ export function migrateLogicRootForEditor(
|
||||
return repairPriceExtremePairForest(migrated.root, migrated.orphans);
|
||||
}
|
||||
|
||||
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거; 직접입력도 hline 역할과 일치하면 HL_* 로 통일 */
|
||||
/** 저장·표시·평가 공통 — K_50 등을 HL_MID 로 접기 */
|
||||
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
|
||||
let next: ConditionDSL = { ...cond };
|
||||
let next: ConditionDSL = {
|
||||
...cond,
|
||||
indicatorType: cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType,
|
||||
};
|
||||
const targetNum = coerceTargetValue(next.targetValue);
|
||||
|
||||
if (isThresholdOverridden(next)) {
|
||||
if (next.targetValue != null && Number.isFinite(next.targetValue)) {
|
||||
const role = inferThresholdRoleForValue(next.targetValue, next.indicatorType, {});
|
||||
if (targetNum != null) {
|
||||
const role = inferThresholdRoleForValue(targetNum, next.indicatorType, {});
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = next;
|
||||
next = { ...rest, rightField: role, thresholdOverride: false };
|
||||
@@ -168,13 +194,21 @@ export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionD
|
||||
}
|
||||
|
||||
function normalizeLogicNode(node: LogicNode): LogicNode {
|
||||
let next = node;
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
next = { ...node, condition: normalizeConditionForPersistence(node.condition) };
|
||||
let next: LogicNode = node;
|
||||
if (isConditionCarrier(node)) {
|
||||
next = {
|
||||
...node,
|
||||
type: node.type ?? 'CONDITION',
|
||||
condition: normalizeConditionForPersistence(node.condition),
|
||||
};
|
||||
}
|
||||
if (next.children?.length) {
|
||||
next = { ...next, children: next.children.map(normalizeLogicNode) };
|
||||
}
|
||||
const legacyChild = (next as LogicNode & { child?: LogicNode }).child;
|
||||
if (legacyChild) {
|
||||
next = { ...next, child: normalizeLogicNode(legacyChild) } as LogicNode;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,17 @@ export function hydrateStrategyDto(strategy: StrategyDto): StrategyDto {
|
||||
if (!layout) return next;
|
||||
|
||||
try {
|
||||
const buyState = decodeConditionForEditor(buy);
|
||||
const sellState = decodeConditionForEditor(sell);
|
||||
const normalizedBuy = asLogicNode(next.buyCondition);
|
||||
const normalizedSell = asLogicNode(next.sellCondition);
|
||||
const buyState = decodeConditionForEditor(normalizedBuy);
|
||||
const sellState = decodeConditionForEditor(normalizedSell);
|
||||
const encodedBuy = encodeConditionForSave(buyState);
|
||||
const encodedSell = encodeConditionForSave(sellState);
|
||||
if (encodedBuy || encodedSell) {
|
||||
next = {
|
||||
...next,
|
||||
buyCondition: encodedBuy ?? buy ?? undefined,
|
||||
sellCondition: encodedSell ?? sell ?? undefined,
|
||||
buyCondition: encodedBuy ?? normalizedBuy ?? undefined,
|
||||
sellCondition: encodedSell ?? normalizedSell ?? undefined,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -38,7 +38,8 @@ function mergedHlThresh(
|
||||
indicatorType: string,
|
||||
hlThresh: Record<string, HlThresh>,
|
||||
): HlThresh {
|
||||
return { ...DEFAULT_HL_THRESH[indicatorType], ...hlThresh[indicatorType] };
|
||||
const key = indicatorType?.toUpperCase?.() ?? indicatorType;
|
||||
return { ...DEFAULT_HL_THRESH[key], ...hlThresh[key], ...hlThresh[indicatorType] };
|
||||
}
|
||||
|
||||
export function defaultThresholdForRole(
|
||||
|
||||
Reference in New Issue
Block a user