Files
vyndr/tests/unit/heroPropService.test.js
T
builtbykev 7a925f43eb Model Train arc 1 (engine): de-vig + EV + takeable/value gates + hero v2 + triplet
Steps 1-6 — make "real opportunities at takeable prices" the engine, not a filter.

1. DE-VIG (src/utils/devig.js): two-way multiplicative de-vig strips the vig and
   returns fair prob + fair price per side + the overround. One side missing →
   fair UNAVAILABLE (null), never faked. Method noted in code + the `devig_method`
   field.
2. EV (devig.evPct): ev_pct = model prob × decimal − 1 at the graded side's
   ACTUAL price. This is the ranking signal now, replacing raw |model−consensus|.
3. TAKEABLE gate (src/config/valueEngine.js, TAKEABLE_ODDS_CEILING −160 .. +200,
   env-tunable): promoted surfaces only (hero/featured/alerts). The full board
   still shows everything; Parlay Lab exempt; JUICE_ODDS_FLOOR (−400) stays the
   absolute backstop underneath. Strict null-guard (Number(null)===0 would have
   made a missing price "takeable").
4. VALUE flag: passes BOTH gates (takeable AND ev_pct ≥ VALUE_EV_THRESHOLD).
   Grade = read quality; value = the price pays you. Shipped in payloads.
5. HERO v2 (heroPropService): highest ev_pct among takeable A/B reads — a huge
   gap on a −900 line is trivia, not an opportunity.
6. VALUE TRIPLET: book_odds · fair_odds · model_odds on every read (snapshot,
   hero, scan — they all spread the grade). Handoff documents the fields; the
   rendering is Session-2 Design's job.

All wired in analyzeViaEngine1's existing p_win/kelly block (real quantile
probability × real book odds, or nothing). 33 new tests; suite 276/3306 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 02:43:30 -04:00

77 lines
3.5 KiB
JavaScript

'use strict';
// Hero rule v2 (Model Train, step 5): highest ev_pct among reads passing the
// TAKEABLE gate, A/B grades only. Empty slate → most recent real read.
const { pickHeroProp } = require('../../src/services/heroPropService');
function cacheFrom(map) { return async (key) => (key in map ? map[key] : null); }
// A graded read carries ev_pct + book_odds (the v2 ranking inputs).
const grade = (o) => ({
player_name: o.player, stat_type: o.stat || 'hits', line: o.line ?? 1.5, projection: o.proj ?? 2.0,
direction: o.dir || 'over', grade: o.grade, book: o.book || 'dk',
ev_pct: o.ev, book_odds: o.odds ?? -120, value: o.value ?? null,
gradedAt: { line: o.line ?? 1.5, odds: o.odds ?? -120, timestamp: o.ts || '2026-07-17T19:00:00Z' },
});
describe('pickHeroProp — v2 (EV among takeable A/B)', () => {
test('picks the HIGHEST ev_pct among takeable A/B reads', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'LowEV', grade: 'A', ev: 3.1, odds: -120 }),
grade({ player: 'HighEV', grade: 'B', ev: 8.4, odds: +110 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.available).toBe(true);
expect(hero.player).toBe('HighEV');
expect(hero.ev_pct).toBe(8.4);
});
test('a HUGE EV on an un-takeable price (-900) is NOT the hero (trivia, not opportunity)', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'Chalk', grade: 'A', ev: 20, odds: -900 }), // un-takeable
grade({ player: 'Takeable', grade: 'B', ev: 5, odds: -130 }), // in band
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.player).toBe('Takeable');
});
test('C/D/F grades are ineligible even with high EV', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'HighEV_C', grade: 'C', ev: 12, odds: -110 }),
grade({ player: 'RealA', grade: 'A', ev: 4, odds: -110 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.player).toBe('RealA');
});
test('exposes the value triplet + ev/value on the hero', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
{ player_name: 'Trip', stat_type: 'hits', line: 1.5, projection: 2.1, direction: 'over',
grade: 'A', book: 'dk', ev_pct: 6.2, value: true, takeable: true,
book_odds: -145, fair_odds: -132, model_odds: -110,
gradedAt: { line: 1.5, odds: -145, timestamp: '2026-07-17T19:00:00Z' } },
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.book_odds).toBe(-145);
expect(hero.fair_odds).toBe(-132);
expect(hero.model_odds).toBe(-110);
expect(hero.ev_pct).toBe(6.2);
expect(hero.value).toBe(true);
});
test('no takeable A/B EV read → most recent real read (fallback)', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'OnlyChalk', grade: 'A', ev: 9, odds: -800, ts: '2026-07-17T19:00:00Z' }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.available).toBe(true);
expect(hero.is_recent).toBe(true);
expect(hero.player).toBe('OnlyChalk');
});
test('nothing cached → { available:false }', async () => {
const hero = await pickHeroProp({ cacheGet: cacheFrom({}), sports: ['mlb'] });
expect(hero).toEqual({ available: false });
});
});