직접입력값 평가오류 수정
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
isThresholdFieldOrSymbol,
|
||||
isThresholdSymbol,
|
||||
inferThresholdRoleForValue,
|
||||
inferThresholdSymbolFromLegacyExact,
|
||||
resolveInheritedThresholdField,
|
||||
resolveThresholdFromDef,
|
||||
THRESHOLD_HL_MID,
|
||||
@@ -204,6 +205,38 @@ export function thresholdField(value: number): string {
|
||||
return `K_${value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 표준 hline 값(30/50/70 등)이면 K_*·targetValue 를 HL_* 로 통일.
|
||||
* 차트 커스텀 hline(def.hlThresh)과 무관 — Ta4j 기본 역할값 기준.
|
||||
*/
|
||||
export function canonicalizeConditionThreshold(cond: ConditionDSL): ConditionDSL {
|
||||
const indicatorType = cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType;
|
||||
let next: ConditionDSL = { ...cond, indicatorType };
|
||||
|
||||
const rawTarget = next.targetValue;
|
||||
const targetNum = typeof rawTarget === 'number' && Number.isFinite(rawTarget) ? rawTarget : null;
|
||||
const fromField = parseThresholdField(next.rightField);
|
||||
const val = Number.isFinite(targetNum) ? targetNum : fromField;
|
||||
|
||||
if (val != null && Number.isFinite(val)) {
|
||||
const role = inferThresholdRoleForValue(val, indicatorType, {});
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = next;
|
||||
return { ...rest, rightField: role, thresholdOverride: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (next.rightField?.startsWith('K_')) {
|
||||
const role = inferThresholdSymbolFromLegacyExact(next.rightField, indicatorType, {});
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = next;
|
||||
return { ...rest, rightField: role, thresholdOverride: false };
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export function hasEditableThreshold(cond: ConditionDSL): boolean {
|
||||
return isThresholdFieldOrSymbol(cond.rightField)
|
||||
|| isThresholdFieldOrSymbol(cond.leftField)
|
||||
@@ -280,7 +313,9 @@ 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);
|
||||
return canonicalizeConditionThreshold(
|
||||
setConditionThreshold(cond, side, value, def),
|
||||
);
|
||||
}
|
||||
|
||||
export function setConditionThreshold(
|
||||
@@ -294,9 +329,7 @@ export function setConditionThreshold(
|
||||
const current = cond[fieldKey];
|
||||
if (!isThresholdFieldOrSymbol(current)) {
|
||||
if (side === 'right' && INDICATORS_WITH_RIGHT_THRESHOLD_INPUT.has(cond.indicatorType)) {
|
||||
const matchedRole = def
|
||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
||||
: null;
|
||||
const matchedRole = inferThresholdRoleForValue(value, cond.indicatorType, {});
|
||||
if (matchedRole != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
||||
@@ -318,10 +351,8 @@ export function setConditionThreshold(
|
||||
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
||||
: null;
|
||||
|
||||
// 30/50/70 등 hline 역할과 정확히 일치하면 직접입력·프리셋 모두 HL_* 로 통일 (평가 일치)
|
||||
const matchedRole = def
|
||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
||||
: null;
|
||||
// 30/50/70 등 표준 hline — 차트 커스텀값과 무관하게 HL_* 로 통일 (직접입력·프리셋·평가 일치)
|
||||
const matchedRole = inferThresholdRoleForValue(value, cond.indicatorType, {});
|
||||
if (matchedRole != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
||||
@@ -335,6 +366,12 @@ export function setConditionThreshold(
|
||||
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
||||
}
|
||||
|
||||
const fallbackRole = inferThresholdRoleForValue(value, cond.indicatorType, {});
|
||||
if (fallbackRole != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return { ...rest, [fieldKey]: fallbackRole, thresholdOverride: false };
|
||||
}
|
||||
|
||||
return {
|
||||
...cond,
|
||||
[fieldKey]: thresholdField(value),
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ConditionDSL, LogicNode } from './strategyTypes';
|
||||
import type { DefType } from './strategyEditorShared';
|
||||
import { getDefaultConditionFields } from './strategyEditorShared';
|
||||
import {
|
||||
canonicalizeConditionThreshold,
|
||||
isThresholdOverridden,
|
||||
isValuePeriodOverridden,
|
||||
parseThresholdField,
|
||||
@@ -43,14 +44,14 @@ function migrateConditionThreshold(
|
||||
if (isThresholdOverridden(cond)) {
|
||||
const val = coerceTargetValue(cond.targetValue) ?? parseThresholdField(cond.rightField);
|
||||
if (val != null) {
|
||||
const role = inferThresholdRoleForValue(val, cond.indicatorType, def.hlThresh);
|
||||
const role = inferThresholdRoleForValue(val, cond.indicatorType, {});
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
||||
}
|
||||
}
|
||||
if (cond.rightField?.startsWith('K_')) {
|
||||
const role = inferThresholdSymbolFromLegacyExact(cond.rightField, cond.indicatorType, def.hlThresh);
|
||||
const role = inferThresholdSymbolFromLegacyExact(cond.rightField, cond.indicatorType, {});
|
||||
if (role != null) {
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
||||
@@ -61,8 +62,8 @@ function migrateConditionThreshold(
|
||||
|
||||
let rightField = cond.rightField;
|
||||
if (rightField?.startsWith('K_')) {
|
||||
rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, def.hlThresh)
|
||||
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, def.hlThresh);
|
||||
rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, {})
|
||||
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, {});
|
||||
}
|
||||
if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) {
|
||||
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
|
||||
@@ -139,47 +140,7 @@ export function migrateLogicRootForEditor(
|
||||
|
||||
/** 저장·표시·평가 공통 — K_50 등을 HL_MID 로 접기 */
|
||||
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
|
||||
let next: ConditionDSL = {
|
||||
...cond,
|
||||
indicatorType: cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType,
|
||||
};
|
||||
const targetNum = coerceTargetValue(next.targetValue);
|
||||
|
||||
if (isThresholdOverridden(next)) {
|
||||
if (targetNum != null) {
|
||||
const role = inferThresholdRoleForValue(targetNum, 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(
|
||||
next.rightField,
|
||||
next.indicatorType,
|
||||
{},
|
||||
) ?? defaultInheritedThresholdField(next.indicatorType);
|
||||
}
|
||||
if (!isThresholdSymbol(next.rightField)) {
|
||||
const role = next.indicatorType === 'DISPARITY' || next.indicatorType === 'VOLUME_OSC'
|
||||
|| next.indicatorType === 'TRIX'
|
||||
? THRESHOLD_HL_MID : THRESHOLD_HL_OVER;
|
||||
if (parseThresholdField(next.rightField) != null || !next.rightField) {
|
||||
next.rightField = role;
|
||||
}
|
||||
}
|
||||
const { targetValue: _t, ...rest } = next;
|
||||
next = { ...rest, thresholdOverride: false };
|
||||
}
|
||||
let next = canonicalizeConditionThreshold(cond);
|
||||
|
||||
if (!isValuePeriodOverridden(next)) {
|
||||
const prefix = VALUE_FIELD_PREFIX[next.indicatorType];
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getCompositeFieldOpts,
|
||||
getCompositeLeftCandleType,
|
||||
getCompositeRightCandleType,
|
||||
canonicalizeConditionThreshold,
|
||||
parseThresholdField,
|
||||
} from './conditionPeriods';
|
||||
import { formatStrategyCandleLabel } from './strategyStartNodes';
|
||||
@@ -149,10 +150,11 @@ function describeConditionType(
|
||||
}
|
||||
|
||||
function describeCondition(
|
||||
cond: ReturnType<typeof normalizeCompositeCondition>,
|
||||
raw: ReturnType<typeof normalizeCompositeCondition>,
|
||||
signalType: 'buy' | 'sell',
|
||||
def: DefType,
|
||||
): string {
|
||||
const cond = canonicalizeConditionThreshold(raw);
|
||||
const ind = cond.indicatorType;
|
||||
const ct = cond.conditionType;
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
||||
import {
|
||||
activateDirectThresholdInput,
|
||||
canonicalizeConditionThreshold,
|
||||
getCompositeFieldOpts,
|
||||
getCompositePeriodPresetOptions,
|
||||
getConditionRightPeriod,
|
||||
@@ -923,7 +924,7 @@ export function resolveFieldOptionValue(indicatorType: string, field?: string):
|
||||
export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
|
||||
if (!node) return '';
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
const c = normalizeCompositeCondition(node.condition);
|
||||
const c = canonicalizeConditionThreshold(normalizeCompositeCondition(node.condition));
|
||||
const indName = getStrategyIndicatorDisplayName(c.indicatorType);
|
||||
const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
|
||||
const C = condLabel(c.conditionType);
|
||||
@@ -1154,7 +1155,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
const handleRight = (v: string) => {
|
||||
if (isThresholdSymbol(v)) {
|
||||
const { targetValue: _t, ...rest } = normalized;
|
||||
onChange({ ...rest, rightField: v, thresholdOverride: false });
|
||||
onChange(canonicalizeConditionThreshold({ ...rest, rightField: v, thresholdOverride: false }));
|
||||
return;
|
||||
}
|
||||
const thresholds: Record<string,number> = {
|
||||
@@ -1176,7 +1177,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
|
||||
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
|
||||
}
|
||||
onChange(upd);
|
||||
onChange(canonicalizeConditionThreshold(upd));
|
||||
};
|
||||
|
||||
if (normalized.composite) {
|
||||
@@ -1441,7 +1442,11 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
def,
|
||||
));
|
||||
}}
|
||||
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def, { directInput: true }))}
|
||||
onCustomNumberChange={v => onChange(
|
||||
canonicalizeConditionThreshold(
|
||||
setConditionThreshold(normalized, 'right', v, def, { directInput: true }),
|
||||
),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* 조건 */}
|
||||
|
||||
Reference in New Issue
Block a user