/** 정규화 로직 회귀 검증 — node scripts/test-threshold-normalize.mjs */ function isThresholdOverridden(cond) { return cond.thresholdOverride === true; } function parseThresholdField(field) { if (!field?.startsWith('K_')) return null; const n = parseFloat(field.slice(2)); return Number.isFinite(n) ? n : null; } const DEFAULT = { RSI: { over: 70, mid: 50, under: 30 } }; function inferThresholdRoleForValue(value, indicatorType) { const key = indicatorType?.toUpperCase?.() ?? indicatorType; const th = DEFAULT[key] ?? {}; const eps = 0.0001; if (th.over != null && Math.abs(th.over - value) < eps) return 'HL_OVER'; if (th.mid != null && Math.abs(th.mid - value) < eps) return 'HL_MID'; if (th.under != null && Math.abs(th.under - value) < eps) return 'HL_UNDER'; return null; } function inferFromK(field, indicatorType) { if (!field?.startsWith('K_')) return null; const val = parseFloat(field.slice(2)); return inferThresholdRoleForValue(val, indicatorType); } function canonicalizeCondition(cond) { let next = { ...cond, indicatorType: cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType }; const targetNum = typeof next.targetValue === 'number' && Number.isFinite(next.targetValue) ? next.targetValue : null; const fromField = parseThresholdField(next.rightField); const val = targetNum ?? fromField; if (val != null && Number.isFinite(val)) { const role = inferThresholdRoleForValue(val, next.indicatorType); if (role) { const { targetValue, ...rest } = next; return { ...rest, rightField: role, thresholdOverride: false }; } } if (next.rightField?.startsWith('K_')) { const role = inferFromK(next.rightField, next.indicatorType); if (role) { const { targetValue, ...rest } = next; return { ...rest, rightField: role, thresholdOverride: false }; } } return next; } function isConditionCarrier(node) { return !!node.condition && (node.type === 'CONDITION' || node.type == null); } function normalizeNode(node) { let next = { ...node }; if (isConditionCarrier(node)) { next = { ...node, type: node.type ?? 'CONDITION', condition: canonicalizeCondition(node.condition) }; } if (next.children?.length) { next.children = next.children.map(normalizeNode); } 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 = [ { type: 'CONDITION', condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } }, { condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } }, { type: 'TIMEFRAME', children: [{ condition: { indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50 } }] }, ]; for (const c of cases) { const r = normalizeNode(c); const cond = r.condition ?? r.children?.[0]?.condition; 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); } 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);