const pe = require('../../src/services/intelligence/probabilityEstimator'); function logsAt(values) { return values.map((v) => ({ points: v })); } describe('probabilityEstimator.estimateProbability', () => { test('30-pt scorer with 25.5 line → p_over > 0.70', () => { const r = pe.estimateProbability({ gameLogs: logsAt([32, 28, 31, 27, 33, 29, 30, 26, 35, 28]), line: 25.5, statType: 'points', }); expect(r.p_over).toBeGreaterThan(0.70); expect(r.p_under).toBeCloseTo(1 - r.p_over, 5); }); test('20-pt scorer with 25.5 line → p_over < 0.40', () => { const r = pe.estimateProbability({ gameLogs: logsAt([18, 22, 19, 21, 20, 17, 23, 19, 18, 22]), line: 25.5, statType: 'points', }); expect(r.p_over).toBeLessThan(0.40); }); test('home game bumps p_over by ~0.015', () => { const base = pe.estimateProbability({ gameLogs: logsAt([28, 26, 27, 25, 24]), line: 25.5, statType: 'points', features: {}, }); const home = pe.estimateProbability({ gameLogs: logsAt([28, 26, 27, 25, 24]), line: 25.5, statType: 'points', features: { home_away: 1.0 }, }); expect(home.p_over - base.p_over).toBeCloseTo(0.015, 5); }); test('weak opponent (rank >= 0.70) adds ~0.03', () => { const base = pe.estimateProbability({ gameLogs: logsAt([26, 25, 27, 24, 26]), line: 25.5, statType: 'points', }); const weakOpp = pe.estimateProbability({ gameLogs: logsAt([26, 25, 27, 24, 26]), line: 25.5, statType: 'points', features: { opp_rank_stat: 0.85 }, }); expect(weakOpp.p_over - base.p_over).toBeCloseTo(0.03, 5); }); test('top-defense opponent (rank <= 0.30) subtracts ~0.03', () => { const base = pe.estimateProbability({ gameLogs: logsAt([26, 25, 27, 24, 26]), line: 25.5, statType: 'points', }); const stiff = pe.estimateProbability({ gameLogs: logsAt([26, 25, 27, 24, 26]), line: 25.5, statType: 'points', features: { opp_rank_stat: 0.1 }, }); expect(base.p_over - stiff.p_over).toBeCloseTo(0.03, 5); }); test('volatile player (high cv) gets pulled toward 0.50', () => { // Use a 4/5 sample so base p ≈ 0.8 (well below the 0.95 ceiling) // — that way the pull-toward-0.5 has visible room to move. const stable = pe.estimateProbability({ gameLogs: logsAt([28, 24, 29, 27, 30]), line: 25.5, statType: 'points', features: { l10_stddev: 1.0, l20_avg: 27.0 }, // cv ~0.04 }); const wild = pe.estimateProbability({ gameLogs: logsAt([28, 24, 29, 27, 30]), line: 25.5, statType: 'points', features: { l10_stddev: 14.0, l20_avg: 27.0 }, // cv = 0.5, volatile }); expect(wild.p_over).toBeLessThan(stable.p_over); }); test('clamps to [0.10, 0.95]', () => { const ceil = pe.estimateProbability({ gameLogs: logsAt([50, 48, 52, 49, 51]), line: 10, statType: 'points', features: { opp_rank_stat: 0.95, home_away: 1.0 }, }); const floor = pe.estimateProbability({ gameLogs: logsAt([5, 6, 4, 7, 5]), line: 30, statType: 'points', features: { opp_rank_stat: 0.05, home_away: 0.0 }, }); expect(ceil.p_over).toBeLessThanOrEqual(0.95); expect(floor.p_over).toBeGreaterThanOrEqual(0.10); }); test('fewer than 5 games uses all available for recency', () => { const r = pe.estimateProbability({ gameLogs: logsAt([30, 28]), line: 25.5, statType: 'points', }); expect(r.p_over).toBeGreaterThan(0.5); }); test('returns nulls on empty input', () => { const r = pe.estimateProbability({ gameLogs: [], line: 25.5, statType: 'points' }); expect(r.p_over).toBeNull(); expect(r.reason).toBe('insufficient_data'); }); test('all-push sample returns reason all_pushes', () => { const r = pe.estimateProbability({ gameLogs: logsAt([25.5, 25.5, 25.5]), line: 25.5, statType: 'points', }); expect(r.p_over).toBeNull(); expect(r.reason).toBe('all_pushes'); }); });