Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user