/** * Session 64 — DIRECTIONAL CLV (per-read signal only). * * User-facing: a wrong badge is a credibility kill, so the polarity trap is * proven BEFORE the logic exists. The same line move must produce OPPOSITE * signs for an over-grade and an under-grade — a move that helps an over hurts * an under on the very same prop. * * NOT an aggregate CLV stat. Aggregate stays held until backtest-proven. */ const dclv = require('../../src/services/directionalClv'); // Lock: over -110 / under -110 → fair over ≈ 0.500 // Close: over -150 / under +130 → fair over ≈ 0.598 (market moved toward OVER) const LOCK = { over: -110, under: -110 }; const CLOSE_TOWARD_OVER = { over: -150, under: 130 }; describe('POLARITY — the badge-inverting trap', () => { test('a move toward OVER is POSITIVE for an over-graded read', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under, closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under, }); expect(r.state).toBe('positive'); expect(r.clv).toBeGreaterThan(0); }); test('the SAME move is NEGATIVE for an under-graded read', () => { const r = dclv.computeDirectionalClv({ side: 'under', lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under, closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under, }); expect(r.state).toBe('negative'); expect(r.clv).toBeLessThan(0); }); test('the two sides are exact mirrors — sign is SIDE-BOUND, not line-direction', () => { const args = { lockOverOdds: LOCK.over, lockUnderOdds: LOCK.under, closeOverOdds: CLOSE_TOWARD_OVER.over, closeUnderOdds: CLOSE_TOWARD_OVER.under, }; const o = dclv.computeDirectionalClv({ ...args, side: 'over' }); const u = dclv.computeDirectionalClv({ ...args, side: 'under' }); expect(o.clv).toBeCloseTo(-u.clv, 6); }); }); describe('FLAT — threshold is PROBABILITY space, never price space', () => { test('a sub-threshold fair-prob move is FLAT, not a badge', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, closeOverOdds: -112, closeUnderOdds: -108, // tiny move flatThreshold: 0.02, }); expect(r.state).toBe('flat'); }); test('a big PRICE move that is a small PROBABILITY move still respects the prob threshold', () => { // Deep favourite: a 40-cent price move is a small probability move. const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -1000, lockUnderOdds: 700, closeOverOdds: -1040, closeUnderOdds: 740, flatThreshold: 0.02, }); expect(r.state).toBe('flat'); expect(Math.abs(r.clv)).toBeLessThan(0.02); }); }); describe('UNKNOWN — never 0, never a fabricated badge', () => { test('a missed close is UNKNOWN', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, missedReason: 'missed_window', }); expect(r.state).toBe('unknown'); expect(r.clv).toBeNull(); }); test('no close at all is UNKNOWN, not flat', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110 }); expect(r.state).toBe('unknown'); expect(r.clv).toBeNull(); }); test('a doubleheader-ambiguous close is UNKNOWN', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, closeOverOdds: -150, closeUnderOdds: 130, missedReason: 'doubleheader_ambiguous', }); expect(r.state).toBe('unknown'); }); test('a ONE-SIDED close cannot be de-vigged → UNKNOWN, never half-computed', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, closeOverOdds: -150, closeUnderOdds: null, }); expect(r.state).toBe('unknown'); expect(r.clv).toBeNull(); }); test('a one-sided LOCK is UNKNOWN too — both ends must be de-viggable', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: null, closeOverOdds: -150, closeUnderOdds: 130, }); expect(r.state).toBe('unknown'); }); }); describe('SAME de-vig method at both ends', () => { test('an unchanged market yields exactly 0 and reads FLAT', () => { const r = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, closeOverOdds: -110, closeUnderOdds: -110, }); expect(r.clv).toBeCloseTo(0, 9); expect(r.state).toBe('flat'); }); test('a pre-de-vigged lock fair prob may be supplied and must agree', () => { const a = dclv.computeDirectionalClv({ side: 'over', lockOverOdds: -110, lockUnderOdds: -110, closeOverOdds: -150, closeUnderOdds: 130, }); const b = dclv.computeDirectionalClv({ side: 'over', lockFairProb: a.fair_lock, closeOverOdds: -150, closeUnderOdds: 130, }); expect(b.clv).toBeCloseTo(a.clv, 9); }); });