8629021774
P1 name edge cases (BOTH playerName.js copies, kept identical): - normalizeName strips hyphens (display+key): "Jung-hoo Lee" === "Jung Hoo Lee". - nameKey strips single-letter MIDDLE tokens: "Josh H Smith" === "Josh Smith" (keeps first+last; real middle names + collapsed initials untouched). - richie -> richard added to NICKNAMES. P2 polish: - Team Hub names normalized at the source (teamService.getTeamHub) so "J.C. Escarra" renders as "JC Escarra" like the dashboard. - snapshotService dedup keeps the highest-confidence GRADE but the richest DISPLAY (accented "José" over "Jose") so prop rows match the pitcher line. - correlationWarning names the game: "2 legs from the same game (NYY @ BOS)". Backend 2246 -> 2255 tests (+9), 194 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.6 KiB
JavaScript
86 lines
3.6 KiB
JavaScript
// Session 50 — Parlay Lab correlation-score model.
|
|
|
|
const svc = require('../../src/services/parlayService');
|
|
|
|
const leg = (over) => ({ player: 'P', team: 'NYY', game: 'NYY@BOS', stat: 'hits', grade: 'A', ...over });
|
|
|
|
describe('correlationScore', () => {
|
|
it('same player, same game → 0.7', () => {
|
|
expect(svc.correlationScore(leg({ stat: 'hits' }), leg({ stat: 'total_bases' }))).toBe(0.7);
|
|
});
|
|
it('same team, same game, different players → 0.4', () => {
|
|
expect(svc.correlationScore(leg({ player: 'A' }), leg({ player: 'B' }))).toBe(0.4);
|
|
});
|
|
it('same game, different teams → 0.2', () => {
|
|
expect(svc.correlationScore(leg({ player: 'A', team: 'NYY' }), leg({ player: 'B', team: 'BOS' }))).toBe(0.2);
|
|
});
|
|
it('different games → 0.0', () => {
|
|
expect(svc.correlationScore(leg({ game: 'NYY@BOS' }), leg({ game: 'LAD@SF', player: 'B', team: 'LAD' }))).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('combinedGrade', () => {
|
|
it('3 independent A legs → A (no penalty)', () => {
|
|
const r = svc.combinedGrade([
|
|
{ grade: 'A', game: 'g1', team: 't1', player: 'a' },
|
|
{ grade: 'A', game: 'g2', team: 't2', player: 'b' },
|
|
{ grade: 'A', game: 'g3', team: 't3', player: 'c' },
|
|
]);
|
|
expect(r.grade).toBe('A');
|
|
expect(r.penalty).toBe(0);
|
|
});
|
|
it('3 same-team same-game A legs → penalized below A', () => {
|
|
const r = svc.combinedGrade([
|
|
{ grade: 'A', game: 'g1', team: 'NYY', player: 'a' },
|
|
{ grade: 'A', game: 'g1', team: 'NYY', player: 'b' },
|
|
{ grade: 'A', game: 'g1', team: 'NYY', player: 'c' },
|
|
]);
|
|
expect(r.avgCorrelation).toBe(0.4);
|
|
expect(r.penalty).toBeGreaterThan(0);
|
|
expect(['B+', 'B', 'B-', 'A-']).toContain(r.grade);
|
|
expect(svc.gradeScore(r.grade)).toBeLessThan(svc.gradeScore('A'));
|
|
});
|
|
});
|
|
|
|
describe('estimatedPayout', () => {
|
|
it('returns multiplier + discount for a 3-leg parlay', () => {
|
|
const r = svc.estimatedPayout([{ grade: 'A' }, { grade: 'B+' }, { grade: 'A' }], 10);
|
|
expect(r.fairMultiplier).toBeCloseTo(1.25 * 1.45 * 1.25, 2);
|
|
expect(r.correlationDiscount).toBe(1); // independent
|
|
expect(r.payout).toBeGreaterThan(10);
|
|
});
|
|
it('discounts correlated slips', () => {
|
|
const corr = svc.estimatedPayout([{ grade: 'A', game: 'g', team: 'NYY', player: 'a' }, { grade: 'A', game: 'g', team: 'NYY', player: 'b' }], 10);
|
|
expect(corr.correlationDiscount).toBeLessThan(1);
|
|
});
|
|
});
|
|
|
|
describe('correlationWarning', () => {
|
|
it('flags 2+ legs from the same team', () => {
|
|
const w = svc.correlationWarning([
|
|
{ game: 'g', team: 'PHI', player: 'a' }, { game: 'g', team: 'PHI', player: 'b' }, { game: 'g2', team: 'NYY', player: 'c' },
|
|
]);
|
|
expect(w).toBe('⚠ 2 legs from PHI — high correlation');
|
|
});
|
|
it('flags same-game legs on different teams', () => {
|
|
expect(svc.correlationWarning([{ game: 'g', team: 'A', player: 'x' }, { game: 'g', team: 'B', player: 'y' }]))
|
|
.toBe('⚠ 2 legs from the same game (g) — correlated');
|
|
});
|
|
it('null when all legs are independent', () => {
|
|
expect(svc.correlationWarning([{ game: 'g1', team: 'A', player: 'x' }, { game: 'g2', team: 'B', player: 'y' }])).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('gradeParlay (full analysis)', () => {
|
|
it('returns combined + correlation + payout + legs', () => {
|
|
const r = svc.gradeParlay([
|
|
{ grade: 'A', game: 'g', team: 'NYY', player: 'a', stat: 'hits', line: 1.5 },
|
|
{ grade: 'B+', game: 'g', team: 'NYY', player: 'b', stat: 'tb', line: 1.5 },
|
|
], 10);
|
|
expect(r.combined.grade).toBeTruthy();
|
|
expect(r.correlation.warning).toContain('NYY');
|
|
expect(r.payout.amount).toBeGreaterThan(0);
|
|
expect(r.legs).toHaveLength(2);
|
|
});
|
|
});
|