'use strict'; /** * heroPropService (Truth-Everywhere Part 2, item 5) — the DAILY HERO PROP. * * The landing card is a live rule, not a hand-picked example: the graded prop * where VYNDR disagrees MOST with the market — the largest |model - consensus| * gap — gated to A/B grades (conviction, not noise). Highest-confidence would * just be the model agreeing loudly with an undisputed number; the disagreement * is the read that makes a stranger argue. * * Deterministic, no curation. Reads the pre-graded snapshot caches (no grading, * no API credits). Empty slate → the MOST RECENT real graded read with its real * date. Never a hand-written fallback. Truly nothing cached → { available:false } * and the card hides. */ const DEFAULT_SPORTS = ['mlb', 'wnba', 'nba', 'soccer']; // A/B tiers only — the conviction gate. const isAB = (g) => /^[AB]/.test(String(g || '').trim().toUpperCase()); function toHero(g, sport, gap, isRecent) { const at = g.gradedAt || {}; return { available: true, is_recent: !!isRecent, // true = the empty-slate "most recent real read" sport, player: g.player_name || g.player || null, stat_type: g.stat_type || g.stat || null, line: g.line ?? at.line ?? null, // the book's number (consensus) direction: g.direction || 'over', projection: g.projection ?? null, // VYNDR's number (model) grade: g.grade || null, book: g.book || null, graded_at: at.timestamp || null, // real timestamp — "graded 2:14 PM" gap: gap == null ? null : Math.round(gap * 100) / 100, team: g.team || null, reasoning: (g.reasoning && g.reasoning.summary) || null, // blurred paywall teaser // Model Train (steps 2/4/6) — EV ranking, the value flag, and the value // triplet (book price · fair de-vigged price · model price). ev_pct: g.ev_pct ?? null, value: g.value ?? null, takeable: g.takeable ?? null, book_odds: g.book_odds ?? (at.odds != null ? Number(at.odds) : null), fair_odds: g.fair_odds ?? null, model_odds: g.model_odds ?? null, }; } // A prop is a valid hero candidate only with a real projection AND a real line // (both required to compute an honest gap; a missing value never counts as 0). function candidate(g) { const line = Number(g && g.line); const proj = Number(g && g.projection); return Number.isFinite(line) && line > 0 && Number.isFinite(proj) && proj > 0; } async function pickHeroProp(deps = {}) { const cacheGet = deps.cacheGet || require('../utils/redis').cacheGet; const sports = deps.sports || DEFAULT_SPORTS; const all = []; for (const sport of sports) { let grades = null; const snap = await cacheGet(`snapshot:${sport}:latest`); if (snap && Array.isArray(snap.grades)) grades = snap.grades; else { const env = await cacheGet(`grades:${sport}`); if (env && Array.isArray(env.grades)) grades = env.grades; } for (const g of grades || []) { if (!g || !g.grade || g.insufficient_data) continue; all.push({ g, sport }); } } // HERO RULE v2 (Model Train, step 5): the highest EV among reads that pass the // TAKEABLE gate, A/B grades only. A huge model-vs-line gap on a -900 line is // trivia; the hero is the best OPPORTUNITY at a price you'd actually take. const { isTakeable } = require('../config/valueEngine'); let hero = null, heroEv = -Infinity; for (const { g, sport } of all) { if (!isAB(g.grade) || !candidate(g)) continue; if (!Number.isFinite(Number(g.ev_pct))) continue; // need a real EV to rank if (!isTakeable(g.book_odds)) continue; // promoted surface → takeable only if (Number(g.ev_pct) > heroEv) { heroEv = Number(g.ev_pct); hero = { g, sport }; } } if (hero) { const gap = candidate(hero.g) ? Math.abs(Number(hero.g.projection) - Number(hero.g.line)) : null; return toHero(hero.g, hero.sport, gap, false); } // Empty slate → the MOST RECENT real graded read (any grade), by timestamp. let recent = null, recentTs = ''; for (const { g, sport } of all) { const ts = (g.gradedAt && g.gradedAt.timestamp) || ''; if (ts && ts > recentTs) { recentTs = ts; recent = { g, sport }; } } if (recent) { const gp = candidate(recent.g) ? Math.abs(Number(recent.g.projection) - Number(recent.g.line)) : null; return toHero(recent.g, recent.sport, gp, true); } // Truly nothing cached — the card hides. Never a fabricated fallback. return { available: false }; } module.exports = { pickHeroProp, __internals: { isAB, candidate, DEFAULT_SPORTS } };