직접입력값 평가오류 수정
This commit is contained in:
+2
@@ -27,6 +27,8 @@ public final class StrategyConditionThresholdNormalizer {
|
|||||||
JsonNode cond = node.get("condition");
|
JsonNode cond = node.get("condition");
|
||||||
if (cond != null && cond.isObject()) {
|
if (cond != null && cond.isObject()) {
|
||||||
normalizeCondition((ObjectNode) cond);
|
normalizeCondition((ObjectNode) cond);
|
||||||
|
} else if (node.has("indicatorType") && node.has("conditionType")) {
|
||||||
|
normalizeCondition((ObjectNode) node);
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode children = node.get("children");
|
JsonNode children = node.get("children");
|
||||||
|
|||||||
@@ -216,10 +216,22 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
case "OR" -> buildOrRule(node, ctx);
|
case "OR" -> buildOrRule(node, ctx);
|
||||||
case "NOT" -> buildNotRule(node, ctx);
|
case "NOT" -> buildNotRule(node, ctx);
|
||||||
case "TIMEFRAME" -> buildTimeframeRule(node, ctx);
|
case "TIMEFRAME" -> buildTimeframeRule(node, ctx);
|
||||||
default -> buildConditionRule(node.path("condition"), ctx);
|
default -> buildConditionRuleNode(node, ctx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CONDITION 래퍼 — nested condition 또는 인라인 조건 필드 모두 지원 */
|
||||||
|
private Rule buildConditionRuleNode(JsonNode node, RuleBuildContext ctx) {
|
||||||
|
JsonNode nested = node.path("condition");
|
||||||
|
if (nested != null && !nested.isNull() && nested.has("indicatorType")) {
|
||||||
|
return buildConditionRule(nested, ctx);
|
||||||
|
}
|
||||||
|
if (node.has("indicatorType")) {
|
||||||
|
return buildConditionRule(node, ctx);
|
||||||
|
}
|
||||||
|
return new BooleanRule(false);
|
||||||
|
}
|
||||||
|
|
||||||
/** 조건 DSL left/right 필드의 현재 봉 수치 (가상투자 UI 표시용) */
|
/** 조건 DSL left/right 필드의 현재 봉 수치 (가상투자 UI 표시용) */
|
||||||
public Double readConditionFieldValue(JsonNode cond, BarSeries series,
|
public Double readConditionFieldValue(JsonNode cond, BarSeries series,
|
||||||
Map<String, Map<String, Object>> params,
|
Map<String, Map<String, Object>> params,
|
||||||
|
|||||||
@@ -22,37 +22,27 @@ function inferFromK(field, indicatorType) {
|
|||||||
const val = parseFloat(field.slice(2));
|
const val = parseFloat(field.slice(2));
|
||||||
return inferThresholdRoleForValue(val, indicatorType);
|
return inferThresholdRoleForValue(val, indicatorType);
|
||||||
}
|
}
|
||||||
function coerceTargetValue(raw) {
|
function canonicalizeCondition(cond) {
|
||||||
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 normalizeCondition(cond) {
|
|
||||||
let next = { ...cond, indicatorType: cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType };
|
let next = { ...cond, indicatorType: cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType };
|
||||||
const targetNum = coerceTargetValue(next.targetValue);
|
const targetNum = typeof next.targetValue === 'number' && Number.isFinite(next.targetValue)
|
||||||
if (isThresholdOverridden(next)) {
|
? next.targetValue
|
||||||
if (targetNum != null) {
|
: null;
|
||||||
const role = inferThresholdRoleForValue(targetNum, next.indicatorType);
|
const fromField = parseThresholdField(next.rightField);
|
||||||
|
const val = targetNum ?? fromField;
|
||||||
|
if (val != null && Number.isFinite(val)) {
|
||||||
|
const role = inferThresholdRoleForValue(val, next.indicatorType);
|
||||||
if (role) {
|
if (role) {
|
||||||
const { targetValue, ...rest } = next;
|
const { targetValue, ...rest } = next;
|
||||||
next = { ...rest, rightField: role, thresholdOverride: false };
|
return { ...rest, rightField: role, thresholdOverride: false };
|
||||||
}
|
}
|
||||||
} else if (next.rightField?.startsWith('K_')) {
|
}
|
||||||
|
if (next.rightField?.startsWith('K_')) {
|
||||||
const role = inferFromK(next.rightField, next.indicatorType);
|
const role = inferFromK(next.rightField, next.indicatorType);
|
||||||
if (role) {
|
if (role) {
|
||||||
const { targetValue, ...rest } = next;
|
const { targetValue, ...rest } = next;
|
||||||
next = { ...rest, rightField: role, thresholdOverride: false };
|
return { ...rest, rightField: role, thresholdOverride: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!isThresholdOverridden(next) && next.rightField?.startsWith('K_')) {
|
|
||||||
const role = inferFromK(next.rightField, next.indicatorType);
|
|
||||||
if (role) next = { ...next, rightField: role, thresholdOverride: false };
|
|
||||||
}
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
function isConditionCarrier(node) {
|
function isConditionCarrier(node) {
|
||||||
@@ -61,7 +51,7 @@ function isConditionCarrier(node) {
|
|||||||
function normalizeNode(node) {
|
function normalizeNode(node) {
|
||||||
let next = { ...node };
|
let next = { ...node };
|
||||||
if (isConditionCarrier(node)) {
|
if (isConditionCarrier(node)) {
|
||||||
next = { ...node, type: node.type ?? 'CONDITION', condition: normalizeCondition(node.condition) };
|
next = { ...node, type: node.type ?? 'CONDITION', condition: canonicalizeCondition(node.condition) };
|
||||||
}
|
}
|
||||||
if (next.children?.length) {
|
if (next.children?.length) {
|
||||||
next.children = next.children.map(normalizeNode);
|
next.children = next.children.map(normalizeNode);
|
||||||
@@ -69,6 +59,17 @@ function normalizeNode(node) {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 차트 hline mid=48 이어도 직접입력 50 → HL_MID (Ta4j 기본값 기준) */
|
||||||
|
function setConditionThreshold(cond, value) {
|
||||||
|
const role = inferThresholdRoleForValue(value, cond.indicatorType);
|
||||||
|
if (role) {
|
||||||
|
const { targetValue, ...rest } = cond;
|
||||||
|
return { ...rest, rightField: role, thresholdOverride: false };
|
||||||
|
}
|
||||||
|
return { ...cond, rightField: `K_${value}`, targetValue: value, thresholdOverride: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
let failed = 0;
|
||||||
const cases = [
|
const cases = [
|
||||||
{ type: 'CONDITION', condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } },
|
{ type: 'CONDITION', condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } },
|
||||||
{ condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } },
|
{ condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } },
|
||||||
@@ -78,5 +79,16 @@ for (const c of cases) {
|
|||||||
const r = normalizeNode(c);
|
const r = normalizeNode(c);
|
||||||
const cond = r.condition ?? r.children?.[0]?.condition;
|
const cond = r.condition ?? r.children?.[0]?.condition;
|
||||||
const ok = cond?.rightField === 'HL_MID' && cond?.thresholdOverride === false;
|
const ok = cond?.rightField === 'HL_MID' && cond?.thresholdOverride === false;
|
||||||
|
if (!ok) failed++;
|
||||||
console.log(ok ? 'PASS' : 'FAIL', JSON.stringify(c).slice(0, 60), '=>', cond?.rightField, cond?.thresholdOverride);
|
console.log(ok ? 'PASS' : 'FAIL', JSON.stringify(c).slice(0, 60), '=>', cond?.rightField, cond?.thresholdOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const direct = setConditionThreshold(
|
||||||
|
{ indicatorType: 'RSI', rightField: 'HL_OVER', thresholdOverride: false },
|
||||||
|
50,
|
||||||
|
);
|
||||||
|
const directOk = direct.rightField === 'HL_MID' && direct.thresholdOverride === false;
|
||||||
|
if (!directOk) failed++;
|
||||||
|
console.log(directOk ? 'PASS' : 'FAIL', 'setConditionThreshold(50) with chart mid≠50 =>', direct.rightField);
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
setConditionRightPeriod,
|
setConditionRightPeriod,
|
||||||
setConditionThreshold,
|
setConditionThreshold,
|
||||||
setConditionValuePeriod,
|
setConditionValuePeriod,
|
||||||
|
canonicalizeConditionThreshold,
|
||||||
usesValuePeriodField,
|
usesValuePeriodField,
|
||||||
} from '../../utils/conditionPeriods';
|
} from '../../utils/conditionPeriods';
|
||||||
import { compositeDisplayName, compositeElementLabel, COMPOSITE_INDICATOR_ITEMS, switchCompositeIndicatorType } from '../../utils/compositeIndicators';
|
import { compositeDisplayName, compositeElementLabel, COMPOSITE_INDICATOR_ITEMS, switchCompositeIndicatorType } from '../../utils/compositeIndicators';
|
||||||
@@ -168,7 +169,11 @@ export default function ConditionNodeSettings({
|
|||||||
min={thresholdBounds.min}
|
min={thresholdBounds.min}
|
||||||
max={thresholdBounds.max}
|
max={thresholdBounds.max}
|
||||||
allowDecimal
|
allowDecimal
|
||||||
onChange={v => onChange(setConditionThreshold(condition, 'right', v, def, { directInput: true }))}
|
onChange={v => onChange(
|
||||||
|
canonicalizeConditionThreshold(
|
||||||
|
setConditionThreshold(condition, 'right', v, def, { directInput: true }),
|
||||||
|
),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import type { Theme } from '../../types/index';
|
import type { Theme } from '../../types/index';
|
||||||
import type { LogicNode, ConditionDSL } from '../../utils/strategyTypes';
|
import type { LogicNode, ConditionDSL } from '../../utils/strategyTypes';
|
||||||
|
import { normalizeConditionForPersistence } from '../../utils/strategyConditionNormalize';
|
||||||
import {
|
import {
|
||||||
addChild,
|
addChild,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
@@ -716,21 +717,22 @@ function StrategyEditorCanvasInner({
|
|||||||
}, [clearDropPreview, applyDropWithAttach, addOrphanAt, root, orphans]);
|
}, [clearDropPreview, applyDropWithAttach, addOrphanAt, root, orphans]);
|
||||||
|
|
||||||
const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
|
const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
|
||||||
|
const next = normalizeConditionForPersistence(condition);
|
||||||
if (isOrphanNode(orphans, id)) {
|
if (isOrphanNode(orphans, id)) {
|
||||||
onOrphansChange(orphans.map(o => (
|
onOrphansChange(orphans.map(o => (
|
||||||
o.id === id ? { ...o, condition } : o
|
o.id === id ? { ...o, condition: next } : o
|
||||||
)));
|
)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root && findNodeInTree(root, id)) {
|
if (root && findNodeInTree(root, id)) {
|
||||||
onChange(updateNode(root, id, n => ({ ...n, condition })));
|
onChange(updateNode(root, id, n => ({ ...n, condition: next })));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const [sid, branch] of Object.entries(extraRoots)) {
|
for (const [sid, branch] of Object.entries(extraRoots)) {
|
||||||
if (branch && findNodeInTree(branch, id)) {
|
if (branch && findNodeInTree(branch, id)) {
|
||||||
onExtraRootsChange?.({
|
onExtraRootsChange?.({
|
||||||
...extraRoots,
|
...extraRoots,
|
||||||
[sid]: updateNode(branch, id, n => ({ ...n, condition })),
|
[sid]: updateNode(branch, id, n => ({ ...n, condition: next })),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
isThresholdFieldOrSymbol,
|
isThresholdFieldOrSymbol,
|
||||||
isThresholdSymbol,
|
isThresholdSymbol,
|
||||||
inferThresholdRoleForValue,
|
inferThresholdRoleForValue,
|
||||||
|
inferThresholdSymbolFromLegacyExact,
|
||||||
resolveInheritedThresholdField,
|
resolveInheritedThresholdField,
|
||||||
resolveThresholdFromDef,
|
resolveThresholdFromDef,
|
||||||
THRESHOLD_HL_MID,
|
THRESHOLD_HL_MID,
|
||||||
@@ -204,6 +205,38 @@ export function thresholdField(value: number): string {
|
|||||||
return `K_${value}`;
|
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 {
|
export function hasEditableThreshold(cond: ConditionDSL): boolean {
|
||||||
return isThresholdFieldOrSymbol(cond.rightField)
|
return isThresholdFieldOrSymbol(cond.rightField)
|
||||||
|| isThresholdFieldOrSymbol(cond.leftField)
|
|| isThresholdFieldOrSymbol(cond.leftField)
|
||||||
@@ -280,7 +313,9 @@ export function activateDirectThresholdInput(
|
|||||||
?? getChartReferenceThreshold(cond, side, inheritedField, def)
|
?? getChartReferenceThreshold(cond, side, inheritedField, def)
|
||||||
?? parseThresholdField(side === 'right' ? cond.rightField : cond.leftField);
|
?? parseThresholdField(side === 'right' ? cond.rightField : cond.leftField);
|
||||||
if (value == null || !Number.isFinite(value)) return cond;
|
if (value == null || !Number.isFinite(value)) return cond;
|
||||||
return setConditionThreshold(cond, side, value, def);
|
return canonicalizeConditionThreshold(
|
||||||
|
setConditionThreshold(cond, side, value, def),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConditionThreshold(
|
export function setConditionThreshold(
|
||||||
@@ -294,9 +329,7 @@ export function setConditionThreshold(
|
|||||||
const current = cond[fieldKey];
|
const current = cond[fieldKey];
|
||||||
if (!isThresholdFieldOrSymbol(current)) {
|
if (!isThresholdFieldOrSymbol(current)) {
|
||||||
if (side === 'right' && INDICATORS_WITH_RIGHT_THRESHOLD_INPUT.has(cond.indicatorType)) {
|
if (side === 'right' && INDICATORS_WITH_RIGHT_THRESHOLD_INPUT.has(cond.indicatorType)) {
|
||||||
const matchedRole = def
|
const matchedRole = inferThresholdRoleForValue(value, cond.indicatorType, {});
|
||||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
|
||||||
: null;
|
|
||||||
if (matchedRole != null) {
|
if (matchedRole != null) {
|
||||||
const { targetValue: _t, ...rest } = cond;
|
const { targetValue: _t, ...rest } = cond;
|
||||||
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
||||||
@@ -318,10 +351,8 @@ export function setConditionThreshold(
|
|||||||
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 30/50/70 등 hline 역할과 정확히 일치하면 직접입력·프리셋 모두 HL_* 로 통일 (평가 일치)
|
// 30/50/70 등 표준 hline — 차트 커스텀값과 무관하게 HL_* 로 통일 (직접입력·프리셋·평가 일치)
|
||||||
const matchedRole = def
|
const matchedRole = inferThresholdRoleForValue(value, cond.indicatorType, {});
|
||||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
|
||||||
: null;
|
|
||||||
if (matchedRole != null) {
|
if (matchedRole != null) {
|
||||||
const { targetValue: _t, ...rest } = cond;
|
const { targetValue: _t, ...rest } = cond;
|
||||||
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
||||||
@@ -335,6 +366,12 @@ export function setConditionThreshold(
|
|||||||
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
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 {
|
return {
|
||||||
...cond,
|
...cond,
|
||||||
[fieldKey]: thresholdField(value),
|
[fieldKey]: thresholdField(value),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ConditionDSL, LogicNode } from './strategyTypes';
|
|||||||
import type { DefType } from './strategyEditorShared';
|
import type { DefType } from './strategyEditorShared';
|
||||||
import { getDefaultConditionFields } from './strategyEditorShared';
|
import { getDefaultConditionFields } from './strategyEditorShared';
|
||||||
import {
|
import {
|
||||||
|
canonicalizeConditionThreshold,
|
||||||
isThresholdOverridden,
|
isThresholdOverridden,
|
||||||
isValuePeriodOverridden,
|
isValuePeriodOverridden,
|
||||||
parseThresholdField,
|
parseThresholdField,
|
||||||
@@ -43,14 +44,14 @@ function migrateConditionThreshold(
|
|||||||
if (isThresholdOverridden(cond)) {
|
if (isThresholdOverridden(cond)) {
|
||||||
const val = coerceTargetValue(cond.targetValue) ?? parseThresholdField(cond.rightField);
|
const val = coerceTargetValue(cond.targetValue) ?? parseThresholdField(cond.rightField);
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
const role = inferThresholdRoleForValue(val, cond.indicatorType, def.hlThresh);
|
const role = inferThresholdRoleForValue(val, cond.indicatorType, {});
|
||||||
if (role != null) {
|
if (role != null) {
|
||||||
const { targetValue: _t, ...rest } = cond;
|
const { targetValue: _t, ...rest } = cond;
|
||||||
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cond.rightField?.startsWith('K_')) {
|
if (cond.rightField?.startsWith('K_')) {
|
||||||
const role = inferThresholdSymbolFromLegacyExact(cond.rightField, cond.indicatorType, def.hlThresh);
|
const role = inferThresholdSymbolFromLegacyExact(cond.rightField, cond.indicatorType, {});
|
||||||
if (role != null) {
|
if (role != null) {
|
||||||
const { targetValue: _t, ...rest } = cond;
|
const { targetValue: _t, ...rest } = cond;
|
||||||
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
return migrateConditionPeriod({ ...rest, rightField: role, thresholdOverride: false });
|
||||||
@@ -61,8 +62,8 @@ function migrateConditionThreshold(
|
|||||||
|
|
||||||
let rightField = cond.rightField;
|
let rightField = cond.rightField;
|
||||||
if (rightField?.startsWith('K_')) {
|
if (rightField?.startsWith('K_')) {
|
||||||
rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, def.hlThresh)
|
rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, {})
|
||||||
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, def.hlThresh);
|
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, {});
|
||||||
}
|
}
|
||||||
if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) {
|
if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) {
|
||||||
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
|
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
|
||||||
@@ -139,47 +140,7 @@ export function migrateLogicRootForEditor(
|
|||||||
|
|
||||||
/** 저장·표시·평가 공통 — K_50 등을 HL_MID 로 접기 */
|
/** 저장·표시·평가 공통 — K_50 등을 HL_MID 로 접기 */
|
||||||
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
|
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
|
||||||
let next: ConditionDSL = {
|
let next = canonicalizeConditionThreshold(cond);
|
||||||
...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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValuePeriodOverridden(next)) {
|
if (!isValuePeriodOverridden(next)) {
|
||||||
const prefix = VALUE_FIELD_PREFIX[next.indicatorType];
|
const prefix = VALUE_FIELD_PREFIX[next.indicatorType];
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
getCompositeFieldOpts,
|
getCompositeFieldOpts,
|
||||||
getCompositeLeftCandleType,
|
getCompositeLeftCandleType,
|
||||||
getCompositeRightCandleType,
|
getCompositeRightCandleType,
|
||||||
|
canonicalizeConditionThreshold,
|
||||||
parseThresholdField,
|
parseThresholdField,
|
||||||
} from './conditionPeriods';
|
} from './conditionPeriods';
|
||||||
import { formatStrategyCandleLabel } from './strategyStartNodes';
|
import { formatStrategyCandleLabel } from './strategyStartNodes';
|
||||||
@@ -149,10 +150,11 @@ function describeConditionType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describeCondition(
|
function describeCondition(
|
||||||
cond: ReturnType<typeof normalizeCompositeCondition>,
|
raw: ReturnType<typeof normalizeCompositeCondition>,
|
||||||
signalType: 'buy' | 'sell',
|
signalType: 'buy' | 'sell',
|
||||||
def: DefType,
|
def: DefType,
|
||||||
): string {
|
): string {
|
||||||
|
const cond = canonicalizeConditionThreshold(raw);
|
||||||
const ind = cond.indicatorType;
|
const ind = cond.indicatorType;
|
||||||
const ct = cond.conditionType;
|
const ct = cond.conditionType;
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import {
|
|||||||
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
||||||
import {
|
import {
|
||||||
activateDirectThresholdInput,
|
activateDirectThresholdInput,
|
||||||
|
canonicalizeConditionThreshold,
|
||||||
getCompositeFieldOpts,
|
getCompositeFieldOpts,
|
||||||
getCompositePeriodPresetOptions,
|
getCompositePeriodPresetOptions,
|
||||||
getConditionRightPeriod,
|
getConditionRightPeriod,
|
||||||
@@ -923,7 +924,7 @@ export function resolveFieldOptionValue(indicatorType: string, field?: string):
|
|||||||
export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
|
export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
|
||||||
if (!node) return '';
|
if (!node) return '';
|
||||||
if (node.type === 'CONDITION' && node.condition) {
|
if (node.type === 'CONDITION' && node.condition) {
|
||||||
const c = normalizeCompositeCondition(node.condition);
|
const c = canonicalizeConditionThreshold(normalizeCompositeCondition(node.condition));
|
||||||
const indName = getStrategyIndicatorDisplayName(c.indicatorType);
|
const indName = getStrategyIndicatorDisplayName(c.indicatorType);
|
||||||
const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
|
const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
|
||||||
const C = condLabel(c.conditionType);
|
const C = condLabel(c.conditionType);
|
||||||
@@ -1154,7 +1155,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
const handleRight = (v: string) => {
|
const handleRight = (v: string) => {
|
||||||
if (isThresholdSymbol(v)) {
|
if (isThresholdSymbol(v)) {
|
||||||
const { targetValue: _t, ...rest } = normalized;
|
const { targetValue: _t, ...rest } = normalized;
|
||||||
onChange({ ...rest, rightField: v, thresholdOverride: false });
|
onChange(canonicalizeConditionThreshold({ ...rest, rightField: v, thresholdOverride: false }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const thresholds: Record<string,number> = {
|
const thresholds: Record<string,number> = {
|
||||||
@@ -1176,7 +1177,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
|
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
|
||||||
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
|
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
|
||||||
}
|
}
|
||||||
onChange(upd);
|
onChange(canonicalizeConditionThreshold(upd));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (normalized.composite) {
|
if (normalized.composite) {
|
||||||
@@ -1441,7 +1442,11 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
|||||||
def,
|
def,
|
||||||
));
|
));
|
||||||
}}
|
}}
|
||||||
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def, { directInput: true }))}
|
onCustomNumberChange={v => onChange(
|
||||||
|
canonicalizeConditionThreshold(
|
||||||
|
setConditionThreshold(normalized, 'right', v, def, { directInput: true }),
|
||||||
|
),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 조건 */}
|
{/* 조건 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user