Files
vyndr/tests/unit/parlayService.test.js
T

133 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Unit: parlay builder service (Session 28). Pure functions.
const { calculateParlay, detectCorrelation, suggestParlays, __internals } = require('../../src/services/parlayService');
describe('parlayService — odds conversions', () => {
const { americanToDecimal, decimalToAmerican } = __internals;
test('americanToDecimal: +100 → 2.0, -110 → ~1.909', () => {
expect(americanToDecimal(100)).toBeCloseTo(2.0, 5);
expect(americanToDecimal(-110)).toBeCloseTo(1.9091, 3);
});
test('decimalToAmerican round-trips', () => {
expect(decimalToAmerican(2.0)).toBe(100);
expect(decimalToAmerican(1.5)).toBe(-200);
});
});
describe('parlayService — calculateParlay', () => {
test('2-leg combined odds multiply correctly', () => {
// -110 (1.909) × -110 (1.909) = 3.645 → +264 American
const r = calculateParlay([
{ player: 'A', stat: 'points', side: 'over', line: 20, odds: -110, grade: 'B', confidence: 60 },
{ player: 'B', stat: 'hits', side: 'over', line: 1, odds: -110, grade: 'B', confidence: 60, gameId: 'g2' },
]);
expect(r.combinedDecimal).toBeCloseTo(3.645, 2);
expect(r.combinedOdds).toBe(264);
expect(r.legCount).toBe(2);
});
test('3-leg combined odds', () => {
const r = calculateParlay([
{ player: 'A', stat: 'points', odds: 100, grade: 'A', confidence: 70 },
{ player: 'B', stat: 'hits', odds: 100, grade: 'A', confidence: 70, gameId: 'g2' },
{ player: 'C', stat: 'goals', odds: 100, grade: 'A', confidence: 70, gameId: 'g3' },
]);
expect(r.combinedDecimal).toBeCloseTo(8.0, 5); // 2×2×2
expect(r.combinedOdds).toBe(700);
});
test('combined grade is confidence-weighted', () => {
const r = calculateParlay([
{ player: 'A', stat: 'points', odds: -110, grade: 'A', confidence: 90 },
{ player: 'B', stat: 'hits', odds: -110, grade: 'C', confidence: 10, gameId: 'g2' },
]);
// Heavily weighted toward the A leg.
expect(['A-', 'A', 'B+']).toContain(r.combinedGrade);
});
test('payoutPer10 computed', () => {
const r = calculateParlay([{ player: 'A', stat: 'points', odds: 100, grade: 'B' }]);
expect(r.payoutPer10).toBe(20); // $10 at +100 → $20 total
});
test('empty legs → 400 error, not crash', () => {
expect(() => calculateParlay([])).toThrow();
try { calculateParlay([]); } catch (e) { expect(e.statusCode).toBe(400); }
});
test('kill conditions aggregated', () => {
const r = calculateParlay([
{ player: 'A', stat: 'points', odds: -110, grade: 'B', killConditions: ['blowout risk'] },
{ player: 'B', stat: 'hits', odds: -110, grade: 'B', gameId: 'g2' },
]);
expect(r.hasKillCondition).toBe(true);
expect(r.killConditions[0].player).toBe('A');
});
});
describe('parlayService — correlation detection', () => {
test('different games → independent', () => {
const c = detectCorrelation(
{ player: 'A', stat: 'points', gameId: 'g1', team: 'X' },
{ player: 'B', stat: 'assists', gameId: 'g2', team: 'Y' },
);
expect(c.correlated).toBe(false);
expect(c.type).toBe('independent');
});
test('same-game teammates assists+points → positive', () => {
const c = detectCorrelation(
{ player: 'A', stat: 'assists', gameId: 'g1', team: 'X' },
{ player: 'B', stat: 'points', gameId: 'g1', team: 'X' },
);
expect(c.correlated).toBe(true);
expect(c.type).toBe('positive');
});
test('same-game opposing rebounds → negative (they fight)', () => {
const c = detectCorrelation(
{ player: 'A', stat: 'rebounds', gameId: 'g1', team: 'X' },
{ player: 'B', stat: 'rebounds', gameId: 'g1', team: 'Y' },
);
expect(c.correlated).toBe(true);
expect(c.type).toBe('negative');
});
test('same player opposite sides on same prop → negative conflict', () => {
const c = detectCorrelation(
{ player: 'A', stat: 'points', side: 'over', gameId: 'g1' },
{ player: 'A', stat: 'points', side: 'under', gameId: 'g1' },
);
expect(c.type).toBe('negative');
});
test('negative correlation surfaced on the parlay', () => {
const r = calculateParlay([
{ player: 'A', stat: 'rebounds', side: 'over', odds: -110, grade: 'B', gameId: 'g1', team: 'X' },
{ player: 'B', stat: 'rebounds', side: 'over', odds: -110, grade: 'B', gameId: 'g1', team: 'Y' },
]);
expect(r.hasNegativeCorrelation).toBe(true);
expect(r.correlations).toHaveLength(1);
});
});
describe('parlayService — suggestParlays', () => {
const pool = [
{ player: 'A', stat: 'points', odds: -110, grade: 'A', gameId: 'g1', team: 'X' },
{ player: 'B', stat: 'hits', odds: -110, grade: 'A-', gameId: 'g2', team: 'Y' },
{ player: 'C', stat: 'goals', odds: -110, grade: 'B+', gameId: 'g3', team: 'Z' },
{ player: 'D', stat: 'assists', odds: -110, grade: 'B', gameId: 'g4', team: 'W' },
];
test('returns a suggestion of the requested leg count', () => {
const s = suggestParlays(pool, { legs: 3, max: 1 });
expect(s).toHaveLength(1);
expect(s[0].legs).toHaveLength(3);
expect(s[0].combinedGrade).toBeDefined();
});
test('too few props → empty', () => {
expect(suggestParlays([pool[0]], { legs: 3 })).toEqual([]);
});
});