/** 정규화 로직 회귀 검증 — node scripts/test-threshold-normalize.mjs */ function parseThresholdField(field) { if (!field?.startsWith('K_')) return null; const n = parseFloat(field.slice(2)); return Number.isFinite(n) ? n : null; } function ensureDirectThresholdSnapshot(cond) { const indicatorType = cond.indicatorType?.toUpperCase?.() ?? cond.indicatorType; let next = { ...cond, indicatorType }; for (const key of ['rightField', 'leftField']) { const f = next[key]; if (!f?.startsWith('K_')) continue; const parsed = parseThresholdField(f); if (parsed == null) continue; if (next.targetValue == null || !Number.isFinite(next.targetValue)) { next = { ...next, targetValue: parsed, thresholdOverride: true }; } else if (next.thresholdOverride !== true) { next = { ...next, thresholdOverride: true }; } } return next; } function setDirectThreshold(cond, side, value) { const fieldKey = side === 'left' ? 'leftField' : 'rightField'; return { ...cond, [fieldKey]: `K_${value}`, targetValue: value, thresholdOverride: true, }; } function setConditionThreshold(cond, value, directInput) { if (directInput) return setDirectThreshold(cond, 'right', value); return setDirectThreshold(cond, 'right', value); } let failed = 0; function assertOk(label, ok) { if (!ok) failed++; console.log(ok ? 'PASS' : 'FAIL', label); } const k55 = ensureDirectThresholdSnapshot({ indicatorType: 'RSI', rightField: 'K_55', thresholdOverride: true, targetValue: 55, }); assertOk('K_55 preserved', k55.rightField === 'K_55' && k55.targetValue === 55 && k55.thresholdOverride === true); const k50 = ensureDirectThresholdSnapshot({ indicatorType: 'RSI', rightField: 'K_50', thresholdOverride: true, targetValue: 50, }); assertOk('K_50 preserved (not HL_MID)', k50.rightField === 'K_50' && k50.thresholdOverride === true); const direct55 = setConditionThreshold({ indicatorType: 'RSI', rightField: 'HL_MID', thresholdOverride: false }, 55, true); assertOk('directInput 55 => K_55', direct55.rightField === 'K_55' && direct55.targetValue === 55); const filled = ensureDirectThresholdSnapshot({ indicatorType: 'RSI', rightField: 'K_55', thresholdOverride: false }); assertOk('K_55 fills targetValue', filled.targetValue === 55 && filled.thresholdOverride === true); // 레거시 마이그레이션 — K_55 를 HL_MID(50) 로 접지 않음 function migrateK(cond) { if (cond.thresholdOverride === true) return cond; const field = cond.rightField; if (!field?.startsWith('K_')) return cond; const val = parseFloat(field.slice(2)); if (!Number.isFinite(val)) return cond; const DEFAULT = { RSI: { over: 70, mid: 50, under: 30 } }; const th = DEFAULT.RSI; const eps = 0.0001; if (th.over != null && Math.abs(th.over - val) < eps) return { ...cond, rightField: 'HL_OVER', thresholdOverride: false }; if (th.mid != null && Math.abs(th.mid - val) < eps) return { ...cond, rightField: 'HL_MID', thresholdOverride: false }; if (th.under != null && Math.abs(th.under - val) < eps) return { ...cond, rightField: 'HL_UNDER', thresholdOverride: false }; return { ...cond, rightField: field, targetValue: val, thresholdOverride: true }; } const migrated = migrateK({ indicatorType: 'RSI', rightField: 'K_55', thresholdOverride: false }); assertOk('migrate K_55 stays K_55', migrated.rightField === 'K_55' && migrated.targetValue === 55); // 비-임계값 rightField (RSI 신호선 등) — 로드 시 기본 과열선으로 되돌리지 않음 function migrateRightField(cond, signalType = 'buy') { if (cond.thresholdOverride === true) return cond; let rightField = cond.rightField; if (rightField?.startsWith('K_')) return cond; const THRESHOLD = new Set(['HL_OVER', 'HL_MID', 'HL_UNDER']); const RSI_RIGHT_OPTS = ['RSI_SIGNAL', 'NONE', 'HL_OVER', 'HL_MID', 'HL_UNDER']; if (!rightField) { rightField = 'HL_OVER'; } else if (!THRESHOLD.has(rightField) && !rightField.startsWith('K_')) { if (!RSI_RIGHT_OPTS.includes(rightField)) rightField = 'HL_OVER'; } return { ...cond, rightField, thresholdOverride: false }; } const rsiSignal = migrateRightField({ indicatorType: 'RSI', rightField: 'RSI_SIGNAL', conditionType: 'CROSS_UP' }); assertOk('RSI_SIGNAL preserved on load', rsiSignal.rightField === 'RSI_SIGNAL'); process.exit(failed > 0 ? 1 : 0);