'use strict'; /** * consensusRuler + bookRoles — Order Zero Phase 2. * * These tests lock the properties that make the ruler honest, not just the * arithmetic: DFS can never price, different lines can never be averaged, * one-sided quotes can never rule, and a fallback can never masquerade as a * consensus. */ const { consensusFairProb, incumbentFairProb, compareRulers, referenceFairProbs } = require('../../src/services/consensusRuler'); const roles = require('../../src/config/bookRoles'); const q = (book, line, over, under) => ({ book, line, over_odds: over, under_odds: under }); describe('bookRoles — the three-way split', () => { it('DFS platforms can never price: excluded, and absent from REFERENCE', () => { for (const dfs of roles.DFS_PLATFORMS) { expect(roles.REFERENCE_BOOKS.has(dfs)).toBe(false); expect(roles.EXCLUDED_FROM_PRICING.has(dfs)).toBe(true); expect(roles.roleOf(dfs)).toBe('excluded'); } }); it('keeps the six PropLine-phantom books in TAKEABLE for the odds-api path', () => { for (const b of roles.PHANTOM_ON_PROPLINE) expect(roles.TAKEABLE_BOOKS.has(b)).toBe(true); }); it('pinnacle and the exchanges are reference-only, never takeable', () => { for (const b of ['pinnacle', ...roles.EXCHANGES]) { expect(roles.REFERENCE_BOOKS.has(b)).toBe(true); expect(roles.TAKEABLE_BOOKS.has(b)).toBe(false); } }); it('the live ruler is still v1 — v2 is a challenger, not deployed', () => { expect(roles.CURRENT_RULER_VERSION).toBe(roles.RULER_V1); }); }); describe('consensusRuler — what may price', () => { it('a DFS book is ignored even when it is the only book at the line', () => { const r = consensusFairProb([q('prizepicks', 1.5, -119, -119)], 1.5, 'over'); expect(r.source).toBe('none'); expect(r.fair_prob).toBeNull(); }); it('three DFS books do not make a consensus', () => { const r = consensusFairProb([ q('prizepicks', 1.5, -119, -119), q('underdog', 1.5, -118, -118), q('sleeper', 1.5, -120, -120), ], 1.5, 'over'); expect(r.n).toBe(0); expect(r.source).toBe('none'); }); it('never averages across DIFFERENT lines', () => { const r = consensusFairProb([q('novig', 1.5, -104, -104), q('smarkets', 2.5, 200, -240)], 1.5, 'over'); expect(r.n).toBe(1); expect(r.source).toBe('single_book'); // the 2.5 quote is a different market }); it('a one-sided quote cannot rule (it cannot be de-vigged)', () => { const r = consensusFairProb([q('novig', 1.5, -104, -104), { book: 'kalshi', line: 1.5, over_odds: -103 }], 1.5, 'over'); expect(r.n).toBe(1); }); it('counts a book once even if it appears twice', () => { const refs = referenceFairProbs([q('novig', 1.5, -104, -104), q('novig', 1.5, -150, 130)], 1.5, 'over'); expect(refs).toHaveLength(1); }); }); describe('consensusRuler — labelling and the median', () => { it('n>=2 is a labelled consensus stamped v2', () => { const r = consensusFairProb([q('novig', 1.5, -104, -104), q('kalshi', 1.5, -106, -102)], 1.5, 'over'); expect(r.source).toBe('consensus'); expect(r.ruler_version).toBe(roles.RULER_V2); expect(r.n).toBe(2); }); it('n==1 falls back and is labelled single_book stamped v1 — never called consensus', () => { const r = consensusFairProb([q('novig', 1.5, -104, -104)], 1.5, 'over'); expect(r.source).toBe('single_book'); expect(r.ruler_version).toBe(roles.RULER_V1); expect(r.fair_prob).toBeGreaterThan(0); }); it('uses the MEDIAN so one outlier book cannot drag the ruler', () => { const tight = [q('novig', 1.5, -104, -104), q('kalshi', 1.5, -105, -103), q('smarkets', 1.5, -103, -105)]; const withOutlier = [...tight, q('bovada', 1.5, -400, 300)]; const a = consensusFairProb(tight, 1.5, 'over').fair_prob; const b = consensusFairProb(withOutlier, 1.5, 'over').fair_prob; expect(Math.abs(b - a)).toBeLessThan(0.06); // a mean would move far more expect(consensusFairProb(withOutlier, 1.5, 'over').spread).toBeGreaterThan(0.2); }); it('over and under are complementary at a symmetric price', () => { const qs = [q('novig', 1.5, -104, -104), q('kalshi', 1.5, -104, -104)]; const o = consensusFairProb(qs, 1.5, 'over').fair_prob; const u = consensusFairProb(qs, 1.5, 'under').fair_prob; expect(o + u).toBeCloseTo(1, 3); }); it('no reference quote at all returns null, never a fabricated number', () => { const r = consensusFairProb([q('onexbet', 1.5, -110, -110)], 1.5, 'over'); expect(r.fair_prob).toBeNull(); expect(r.source).toBe('none'); }); it('a null line does not become 0 and match a 0-line quote', () => { expect(consensusFairProb([q('novig', 0, -104, -104)], null, 'over').source).toBe('none'); }); }); describe('consensusRuler — the incumbent it is challenging', () => { it('incumbent is literally first-row-wins, and that row can be a DFS book', () => { const qs = [q('prizepicks', 1.5, -119, -119), q('novig', 1.5, -104, -104)]; const inc = incumbentFairProb(qs, 1.5, 'over'); expect(inc.book).toBe('prizepicks'); expect(inc.ruler_version).toBe(roles.RULER_V1); }); it('compareRulers returns a signed delta in probability points', () => { const qs = [q('draftkings', 1.5, -140, 120), q('novig', 1.5, -104, -104), q('kalshi', 1.5, -103, -105)]; const c = compareRulers(qs, 1.5, 'over'); expect(c.incumbent.book).toBe('draftkings'); expect(c.consensus.source).toBe('consensus'); expect(c.delta_pts).toBeLessThan(0); // dk's favourite priced over above the exchanges expect(Math.round(c.delta_pts * 100) / 100).toBeCloseTo((c.consensus.fair_prob - c.incumbent.fair_prob) * 100, 2); }); it('delta is null when either side is unavailable — never 0', () => { expect(compareRulers([q('prizepicks', 1.5, -119, -119)], 1.5, 'over').delta_pts).toBeNull(); }); });