'use strict'; /** * calibrationService — FIT ON HISTORY, APPLY TO TONIGHT. * * The backtest proved the repair works out-of-sample; this is the same * discipline running forward in production. The map is fitted ONLY on outcomes * that have already settled, and applied to props that have not been played. If * that cut is ever relaxed, the map has seen the answer and every number it * produces is fiction that will look excellent in review. * * ── WHAT THE BACKTEST ESTABLISHED (2026-08-04, hits) ───────────────────── * Fitted on game_date < 2026-08-02 (n=589), evaluated on >= (n=383): * * raw 0.746 -> 0.556 0.843 -> 0.564 0.913 -> 0.667 * calibrated 0.477 -> 0.506 0.587 -> 0.580 0.667 -> 0.603 * * Ordering survived (isotonic is monotone — verified, not assumed). Errors at * the top fell from +0.28 to +0.06. And the honest CEILING dropped to 0.667: * once the numbers are truthful, this model has no 80%+ hit reads at all. The * "high-floor parlay" is a 0.667-per-leg proposition, not a 0.9 one. * * ── CERTIFICATION IS BY BAND, NOT A BLANKET FLAG ───────────────────────── * Held-out errors were −0.029 and +0.007 through the middle, but −0.167 at the * bottom and +0.063 at the top. The model is trustworthy over most of its mass * and untrustworthy at both edges, so a single true/false would either throw * away the 72% that works or ship the edges that do not. Only a probability * inside a certified band is marked stackable. */ const cal = require('./calibration'); const { knownNumber } = require('../../utils/known'); /** Default: hold out the most recent quarter of history to certify on. */ const HOLDOUT_FRACTION = 0.35; const MIN_FIT = 200; /** * Build a calibrator from settled rows. * * @param {Array<{p, won, date}>} settled rows STRICTLY BEFORE the props being graded * @returns {object|null} null when there is not enough history — the caller must * then treat every atom as uncalibrated rather than pass it through raw. */ function build(settled, opts = {}) { const rows = (settled || []) .map((r) => ({ p: knownNumber(r && r.p), won: knownNumber(r && r.won), d: String((r && r.date) || '') })) .filter((r) => r.p !== null && r.won !== null) .sort((a, b) => (a.d < b.d ? -1 : a.d > b.d ? 1 : 0)); if (rows.length < (opts.minFit ?? MIN_FIT)) return null; // Split by TIME, not at random: certifying on rows the map was fitted on // always looks perfect, and a random split leaks the future into the fit. const cut = Math.floor(rows.length * (1 - (opts.holdoutFraction ?? HOLDOUT_FRACTION))); const fitRows = rows.slice(0, cut); const certRows = rows.slice(cut); if (fitRows.length < (opts.minFit ?? MIN_FIT) || certRows.length < 60) return null; const map = cal.fitIsotonic(fitRows, { minTotal: opts.minFit ?? MIN_FIT }); if (!map) return null; const corrected = certRows.map((r) => ({ ...r, p: cal.applyIsotonic(map, r.p) })); const bands = cal.certifyBands(corrected, { tolerance: opts.tolerance ?? 0.05, minBin: opts.minBin ?? 40, }); return { map, bands, fit_n: fitRows.length, certify_n: certRows.length, fitted_through: fitRows[fitRows.length - 1].d, certified_through: certRows[certRows.length - 1].d, /** * Calibrate one probability. * `calibrated` is TRUE only inside a certified band — that flag is what * `chain.chainAcross` requires before it will compound anything. */ calibrate(p) { const raw = knownNumber(p); if (raw === null) return { p_raw: null, p_calibrated: null, calibrated: false, reason: 'absent' }; const c = cal.applyIsotonic(map, raw); if (c === null) return { p_raw: raw, p_calibrated: null, calibrated: false, reason: 'no_map_value' }; const inBand = cal.inCertifiedBand(bands, c); return { p_raw: raw, p_calibrated: Math.round(c * 1000) / 1000, calibrated: inBand, reason: inBand ? null : 'outside_certified_band', }; }, }; } /** * Load settled history from the ledger and build a calibrator, POINT-IN-TIME. * * `before` defaults to today, so the fit can only ever use games that are over. * Injectable for tests; returns null rather than a permissive fallback, because * "no calibrator" must mean "nothing is stackable", not "pass the raw numbers * through". */ async function fromLedger(sb, { sport = 'mlb', stat = 'hits', before = null, ...opts } = {}) { if (!sb) return null; const cutoff = before || new Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', }).format(new Date()); const rows = []; for (let from = 0; ; from += 1000) { const { data, error } = await sb.from('ledger_entries') .select('p_win, outcome, game_date, quarantine_reason') .eq('sport', sport).is('user_id', null).eq('stat', stat) .in('outcome', ['hit', 'miss']).not('p_win', 'is', null) .lt('game_date', cutoff) // STRICTLY before — the whole point .range(from, from + 999); if (error || !data || data.length === 0) break; rows.push(...data); if (data.length < 1000) break; } const clean = rows .filter((r) => !(r.quarantine_reason || '').startsWith('nontakeable_book')) .map((r) => ({ p: Number(r.p_win), won: r.outcome === 'hit' ? 1 : 0, date: String(r.game_date) })); const built = build(clean, opts); return built ? { ...built, cutoff } : null; } module.exports = { build, fromLedger, HOLDOUT_FRACTION, MIN_FIT };