hits-v1: built on the right structure, measured honestly, REFUTED
Hits was diagnosed as a family mismatch: 84% of hits rows trade at 0.5, so the stat rides on P(0), and a negative binomial has unbounded support and no notion of opportunity at all. hits-v1 models it as the bounded conversion it is -- N ~ the player's empirical at-bat distribution, hits|N ~ Binomial(N,q), with the multiplier scaling q (conversion) and never N (opportunity). STEP 0 confirmed the inputs before the model existed: 30/30 real ledger players, 100% combined-input coverage. Every read goes through knownRate -- a row with no atBats is dropped, never counted as a 0-at-bat game. It FIRES: 158/159 hits props (99.4%) on the live production snapshot, through the real attachProjection path. Scoping by book IDENTITY rather than price shape kept 94 out-of-promotion-band props on the board, 93 of them modelled -- 59% that a price rule would have deleted. And it LOST. Point-in-time replay (game log truncated strictly before each row's game_date, real grade-time multiplier), hits-only, direction-aligned, n=242: resolution champion 0.195 / ladder 0.048 / hits-v1 0.026. Paired bootstrap on the same rows: hits-v1 - ladder = -0.022, CI95 excluding zero. Not promoted. The value is in what it eliminates. The family was wrong AND the mean was not the constraint -- hits-v1 moved the line-0.5 mean 0.554 -> 0.581 toward a 0.598 base rate while resolution fell. What is left is per-prop discrimination: the ladder's inputs, not its distribution. The pre-registered fallback is recorded as WRONG rather than deleted. It said hits might be genuinely low-resolution for anyone; the champion scores 0.276 on the identical 189 rows, so there is real signal and the ceiling claim was the comfortable reading, not the honest one. Its own control refuted it, and that control was already in hand when the branch was written. hits-v1 stays wired as a challenger writing its own ledger columns so the forward accrual can confirm the backtest. Champion, ladder, ranking, calibration, reference ruler and the four accruing verdicts are byte-identical -- the diff has zero deleted lines. Tests 4,156 green (332 suites); web build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
@@ -19,7 +19,17 @@
|
||||
|
||||
const dist = require('./projection/distribution');
|
||||
const compoundTb = require('./projection/compoundTotalBases');
|
||||
const binomialHits = require('./projection/binomialHits');
|
||||
const matchup = require('./projection/matchupRead');
|
||||
// THE RIGHT TAKEABLE AXIS. Three questions once shared one word; they no longer
|
||||
// do (src/config/takeability.js). This module reads TWO of them, for two
|
||||
// different purposes, and never the deprecated `takeable` field:
|
||||
// isTakeableMarket — book IDENTITY. Decides what is a real market to
|
||||
// MODEL. Thin, one-sided and juiced markets are all
|
||||
// real; a -300 hits-over is a bet you can place.
|
||||
// isWithinPromotionBand — a PRICE policy band. Decides what is worth
|
||||
// SURFACING. Recorded here, never read by the model.
|
||||
const { isTakeableMarket, isWithinPromotionBand } = require('../config/takeability');
|
||||
const parkBase = require('./parkBase');
|
||||
const { NAME_TO_ABBR } = require('./environmentContext');
|
||||
|
||||
@@ -27,6 +37,10 @@ const { NAME_TO_ABBR } = require('./environmentContext');
|
||||
// projection model is byte-identical; the version bump marks the basis so pre-fix
|
||||
// (raw-book) and post-fix (fair) rows never silently mix in the handicapper test.
|
||||
const PROJ_VERSION = 'proj-v1.1';
|
||||
// hits-v1 — the per-stat structural challenger for HITS. Versioned separately
|
||||
// from PROJ_VERSION so a later change to the ladder never silently re-labels
|
||||
// rows that were written by this model.
|
||||
const HITS_VERSION = 'hits-v1';
|
||||
const PRIOR_GAMES = Number(process.env.PROJ_PRIOR_GAMES || 4);
|
||||
const LADDER_MAX = Number(process.env.PROJ_LADDER_MAX || 4);
|
||||
// Combined non-form multiplier bound (Phase B proved <0.12 stacked). A Coors +
|
||||
@@ -228,8 +242,66 @@ function projectProp({
|
||||
} catch { tbCompound = null; }
|
||||
}
|
||||
|
||||
// ── HITS, modelled as the AT-BAT-BOUNDED CONVERSION it is (hits-v1) ───────
|
||||
// A hit is not a low-rate count. A hitter gets N official at-bats and converts
|
||||
// each at rate q, so hits are BOUNDED by opportunity — something a negative
|
||||
// binomial cannot express, since it has unbounded support and no notion of
|
||||
// opportunity at all. Measured on 245 matched settled rows (direction-aligned,
|
||||
// hits only): the current ladder resolves 0.0595 against the champion's
|
||||
// 0.2044, and 84% of hits rows trade at 0.5 — so almost the whole stat is the
|
||||
// single question P(0 hits), which is exactly where the count family hurts
|
||||
// most. HYPOTHESIS, not a claim: the ledger decides.
|
||||
//
|
||||
// CHALLENGER ONLY: written alongside `proj_p_over_line`, never substituted for
|
||||
// it. The current ladder and the champion are byte-identical. Inputs
|
||||
// underivable (thin log, no at-bat counts) → null, and the prop keeps the
|
||||
// current ladder value. Never fabricated.
|
||||
//
|
||||
// MARKET SCOPE READS IDENTITY, NOT PRICE. `market_takeable` comes from the
|
||||
// book, and NOTHING about the price shape excludes a prop from being modelled:
|
||||
// baseball hits markets are genuinely thin, genuinely juiced and genuinely
|
||||
// one-sided, and all three are normal structure rather than a bad quote. The
|
||||
// promotion band is recorded beside it and deliberately never consulted here —
|
||||
// a -300 hits-over is takeable AND outside the band, and both are true at once.
|
||||
let hitsBinom = null;
|
||||
let hitsMarket = null;
|
||||
if (stat === 'hits' && tradedRung != null) {
|
||||
// The graded side's price. `book_odds` is the graded-side price and is the
|
||||
// only one present on a one-sided quote, so it is a genuine fallback rather
|
||||
// than a substitute for the other side. Absent → null, and the promotion
|
||||
// band answers null (an unknown price is not an out-of-band price).
|
||||
const sideOdds = num(direction === 'under'
|
||||
? (grade && grade.under_odds)
|
||||
: (grade && grade.over_odds)) ?? num(grade && grade.book_odds);
|
||||
hitsMarket = {
|
||||
book: (grade && grade.book) || null,
|
||||
market_takeable: isTakeableMarket(grade && grade.book),
|
||||
within_promotion_band: isWithinPromotionBand(sideOdds),
|
||||
one_sided: (grade && grade.over_odds != null) !== (grade && grade.under_odds != null),
|
||||
price_filtered: false, // stated invariant: no price-shape rule gates the model
|
||||
};
|
||||
try {
|
||||
hitsBinom = binomialHits.projectHits({ rows: gameLog, line, multiplier: M });
|
||||
} catch { hitsBinom = null; }
|
||||
}
|
||||
|
||||
return {
|
||||
proj_version: PROJ_VERSION,
|
||||
proj_hits_p_over: hitsBinom ? hitsBinom.p_over_line : null,
|
||||
proj_hits_meta: hitsBinom ? {
|
||||
version: HITS_VERSION,
|
||||
mean: hitsBinom.mean,
|
||||
hit_rate: hitsBinom.hit_rate,
|
||||
hit_rate_base: hitsBinom.hit_rate_base,
|
||||
ab_per_game: hitsBinom.ab_per_game,
|
||||
ab_distribution: hitsBinom.ab_distribution,
|
||||
games_used: hitsBinom.games_used,
|
||||
at_bats_observed: hitsBinom.at_bats_observed,
|
||||
hits_observed: hitsBinom.hits_observed,
|
||||
family: hitsBinom.family,
|
||||
ab_independence_caveat: hitsBinom.ab_independence_caveat,
|
||||
market: hitsMarket,
|
||||
} : (hitsMarket ? { version: HITS_VERSION, market: hitsMarket, reason: 'inputs_underivable' } : null),
|
||||
proj_tb_p_over: tbCompound ? tbCompound.p_over_line : null,
|
||||
proj_tb_meta: tbCompound ? {
|
||||
version: 'tb-v1', mean: tbCompound.mean, rates: tbCompound.rates,
|
||||
@@ -297,5 +369,5 @@ async function attachProjection(grades, deps = {}) {
|
||||
|
||||
module.exports = {
|
||||
projectProp, attachProjection, parkBaselineFromLogs, recencyWeighted, parkFactorFor,
|
||||
PROJ_VERSION, STAT_FIELD, COMBINED_MAX,
|
||||
PROJ_VERSION, HITS_VERSION, STAT_FIELD, COMBINED_MAX,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user