104 lines
4.3 KiB
JavaScript
104 lines
4.3 KiB
JavaScript
/** 정규화 로직 회귀 검증 — 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);
|