// Wave 4A — MARKET-BREADTH / CONSENSUS strip (Step 4). Makes the DeskShowcase // "consensus vs model" claim REAL: the consensus is the MEDIAN book line across // the prop's per-book rows; the model's position is model_value vs that // consensus, signed by the graded side. Doctrine: never fabricate a consensus — // <2 distinct books → null (absent beats invented); a non-numeric line is // ignored, never coerced to 0 (the Number(null)===0 trap). const { computeBreadth, collectBreadth, median } = require('../../web/src/lib/marketBreadth'); describe('median', () => { it('odd length → middle', () => { expect(median([2.5, 1.5, 3.5])).toBe(2.5); }); it('even length → mean of the two middles', () => { expect(median([1.5, 2.5])).toBe(2); }); it('ignores non-finite, empty → null', () => { expect(median([NaN, 1.5, 2.5, null])).toBe(2); expect(median([])).toBeNull(); expect(median(null)).toBeNull(); }); }); describe('computeBreadth — median consensus from ≥2 books', () => { const books = [ { book: 'draftkings', line: 2.5, over_odds: -115 }, { book: 'fanduel', line: 2.5, over_odds: -110 }, { book: 'betmgm', line: 1.5, over_odds: -120 }, ]; it('consensus is the median book LINE; model above → OVER edge (positive, green)', () => { const b = computeBreadth(books, 3.0, 'over'); expect(b.consensus).toBe(2.5); expect(b.bookCount).toBe(3); expect(b.model).toBe(3.0); expect(b.delta).toBeCloseTo(0.5); expect(b.signedEdge).toBeCloseTo(0.5); // model projects ABOVE market → supports OVER expect(b.position).toBe('above'); expect(b.side).toBe('over'); }); it('UNDER read: model BELOW consensus is the edge (signed positive)', () => { const b = computeBreadth(books, 2.0, 'under'); expect(b.consensus).toBe(2.5); expect(b.delta).toBeCloseTo(-0.5); // raw model - consensus stays signed to the market expect(b.signedEdge).toBeCloseTo(0.5); // consensus - model, favors UNDER expect(b.position).toBe('below'); expect(b.side).toBe('under'); }); it('model behind the market on an OVER → negative signed edge (amber/red)', () => { const b = computeBreadth(books, 2.0, 'over'); expect(b.signedEdge).toBeCloseTo(-0.5); expect(b.position).toBe('below'); }); it('<2 DISTINCT books → null (never fabricate a consensus)', () => { expect(computeBreadth([{ book: 'dk', line: 2.5 }], 3, 'over')).toBeNull(); // same book twice is still one opinion → not a consensus expect(computeBreadth([{ book: 'dk', line: 2.5 }, { book: 'dk', line: 1.5 }], 3, 'over')).toBeNull(); expect(computeBreadth([], 3)).toBeNull(); expect(computeBreadth(null, 3)).toBeNull(); }); it('absent model → real consensus present, comparison null (no invented model)', () => { const b = computeBreadth(books, null, 'over'); expect(b.consensus).toBe(2.5); expect(b.bookCount).toBe(3); expect(b.model).toBeNull(); expect(b.delta).toBeNull(); expect(b.signedEdge).toBeNull(); expect(b.position).toBeNull(); }); it('a non-numeric book line is IGNORED, not coerced to 0', () => { const mixed = [{ book: 'dk', line: 2.5 }, { book: 'fd', line: null }, { book: 'mgm', line: 2.5 }]; const b = computeBreadth(mixed, 2.5, 'over'); expect(b.bookCount).toBe(2); expect(b.consensus).toBe(2.5); expect(b.position).toBe('inline'); // model == consensus expect(b.signedEdge).toBe(0); }); }); describe('collectBreadth — ranked list, self-hiding', () => { it('drops <2-book props and ranks by |signedEdge| desc', () => { const items = [ { player: 'A', stat: 'hits', side: 'over', line: 1.5, modelValue: 2.0, books: [{ book: 'dk', line: 1.5 }, { book: 'fd', line: 1.5 }] }, { player: 'B', stat: 'tb', side: 'over', line: 2.5, modelValue: 2.6, books: [{ book: 'dk', line: 2.5 }, { book: 'fd', line: 2.5 }] }, { player: 'C', stat: 'ks', side: 'over', line: 5.5, modelValue: 6, books: [{ book: 'dk', line: 5.5 }] }, // 1 book → dropped ]; const out = collectBreadth(items, 6); expect(out).toHaveLength(2); expect(out[0].player).toBe('A'); // |0.5| edge beats |0.1| expect(out[1].player).toBe('B'); }); it('empty when nothing qualifies (component self-hides)', () => { expect(collectBreadth([{ player: 'C', stat: 'ks', side: 'over', books: [{ book: 'dk', line: 5.5 }] }], 6)).toEqual([]); expect(collectBreadth(null)).toEqual([]); expect(collectBreadth([])).toEqual([]); }); }); describe('source: MarketBreadth component self-hides + colors by sign', () => { const fs = require('fs'); const path = require('path'); const read = (rel) => fs.readFileSync(path.join(__dirname, '..', '..', 'web', 'src', rel), 'utf8'); it('MarketBreadth.tsx colors the edge via the color contract (edgeColor), not raw green', () => { const src = read('components/vyndr/MarketBreadth.tsx'); expect(src).toContain('edgeColor'); }); it('MarketBreadth.tsx self-hides when there is nothing to show', () => { const src = read('components/vyndr/MarketBreadth.tsx'); expect(src).toMatch(/return null/); }); });