dataphil lab  /  MLB shadow test 2026-06-01 → 2026-08-26 10,928 settled predictions

The model that didn’t ship

I built a Monte Carlo simulator that prices MLB hitter props, wrote down a pass/fail gate before testing it, and ran it against live sportsbook lines for 87 days without betting a dollar. It failed. This is the autopsy.

Gate: failed — not deployed Criterion pre-registered 2026-05-30, two days before the first prediction
Settled predictions
10,928877 voided, 145 open
Predicted / observed
.623 / .596Brier 0.2392
Gate cohort ROI
−3.69%±1.59 SE on n = 2,749
Closing line value
+0.002%indistinguishable from zero
Model weight earned
22%vs the 50% implied by the edge formula
FINDING
01

It is a decent forecaster

Across 87 days the model said 62.3% on average and things happened 59.6% of the time. Slightly overconfident, and drifting worse through the season — but the reliability curve tracks the diagonal everywhere the mass sits.

Exhibit A · Reliability
perfect calibration .42 .60 .78 .42 predicted probability .78 observed frequency
Six bins, 30+ observations each, n = 10,928. The curve sits just under the diagonal throughout — consistent mild overconfidence, not a broken model. If the deliverable were “estimate the probability this hitter gets a hit,” this works.
FINDING
02

It is a bad bettor, and the failure is ordered

Betting doesn’t reward being right. It rewards being right where the market is wrong. So sort every prediction by how far the model departed from the de-vigged market price, and look at what happened.

Exhibit B · The inversion
model predicted actually happened
10.7 pt gap .54 .56 .58 .60 .62 .64 .66 110 decile of model-vs-market disagreement model most bearish model most bullish
The predicted line climbs monotonically. The realised line falls. In the decile where the model was most convinced the book was mispriced, it predicted 64.7% and delivered 54.0% on n = 1,092. This is the signature of a signal that is not merely weak but inverted: the size of the model’s disagreement is information about the model’s own error, not the market’s.
DecileMean disagreementPredictedObservednROI
1 — most bearish−4.9%.568.6081,093−7.9%
3−0.2%.610.6171,093−5.9%
5+2.1%.626.6151,092−4.8%
7+4.3%.638.6061,093−4.3%
9+7.2%.645.5901,093−3.7%
10 — most bullish+12.0%.647.5401,092−3.9%

The window that would have fooled me

The pre-registered cohort came in at −3.69% ± 1.59% ROI on n = 2,749 — more than two standard errors below zero, on twice the sample the gate asked for. But look at how it got there.

Exhibit C · Gate cohort by month
+2.2% −3.4% −6.1% JUN · n=384 JUL · n=1,233 AUG · n=1,132 +5% 0 −5%
Flat-stake ROI, error bars ±1 SE. On 2026-07-08 I ran an interim review, saw +2.5% on n = 552, and wrote that the anti-predictive pattern had “weakened.” It hadn’t. It was 0.7 standard errors of nothing, and it evaporated. Graduating on that window would have deployed a −4% strategy with real money, with the data apparently supporting it.

Meanwhile the criterion that was actually pre-registered — closing line value — never moved. Across the 964 legs with a captured closing price, mean CLV is +0.002%; on the high-edge subset, +0.009% on n = 289. Zero, to as many decimals as anyone wants. The closing line never came toward these picks, which is the market saying — quietly, 964 times — that there was nothing there.

FINDING
03

The diagnosis is a weighting error, not a broken model

A model can lose to the market for two very different reasons: it knows nothing, or it knows something and is being used wrong. These have opposite remedies, so it is worth separating them properly rather than by feel.

The test is a horse race. Regress the realised outcome on both forecasts at once, in log-odds space:

logit P(win) = a + b · logit(p_model) + c · logit(p_market)

If the model is pure noise, b collapses to zero once the market price is in the equation. It doesn’t.

Exhibit D · Horse race, n = 10,928
0 +0.231 model forecast z = +2.20 +0.820 market price z = +7.98 coefficient in log-odds space, 95% CI
The model contributes real, independent information at 2.2 standard errors — conditional on already knowing the closing price. Most retail models do not clear that bar. But the relative weights say the model earns about 22% of the vote and the market 78%.

Where that goes wrong

Look at what the edge calculation was actually doing. edge = p_model − p_market treats the two forecasts as equally credible — an implicit 50/50 weight on something that earns 22%. The model was levered at roughly 2.3× its true strength, and the size of that error scales with the size of the disagreement.

Which is exactly the pattern in Finding 02. The further the model departs from the market, the more it is over-weighted, the worse it does. Decile 10 is where the mis-specification is maximal, and decile 10 is where the model dies.

Corroborating: standalone Brier is 0.2392 for the model and 0.2371 for the market price alone. The market is simply the better forecaster of the two. The model’s job was never to beat it outright — only to add something on top — and the horse race says it does, just far less than the edge formula assumed.

So the conclusion is narrow and specific: the edge estimator is mis-specified, not the model. The right operator is a stacked blend at the measured weights —

logit(p_final) = −0.04 + 0.231 · logit(p_model) + 0.820 · logit(p_market)

— with edge computed off that. Under that blend the edges collapse toward zero and essentially nothing on this market clears the vig. Which is the honest answer, and a different sentence from “the model doesn’t work.”

METHOD

The gate was written down before the test

On 2026-05-30, two days before the first prediction was logged, this went into the project’s methodology document at v0.2.0, §8:

Graduate the simulator to live firing only after N ≥ 200 shadow predictions accumulate with mean CLV > 0 AND calibration holding. Falling short on CLV after N=200 is a finding, not a failure — it means the backtest edge did not survive contact with market prices, and the model needs attribution work before risking bankroll.

Two things about that paragraph matter more than anything else in the project.

First, the bar is closing line value, not profit. Whether a bet won is mostly noise at any realistic sample size. Whether the closing line moved toward your side is a far higher-signal test of whether you knew something the market didn’t. Grading on profit is how people talk themselves into deploying noise.

Second, it says in advance what a negative result would mean, so that when one arrived it couldn’t be relitigated. That is the entire point of pre-registration, and it’s the part of this work I’d defend hardest.

Settlement hygiene, where track records go to die

Players get scratched. If a leg never settles it sits in the log as a blank, and blanks are invisible to every summary statistic — so a record quietly drops its inconvenient rows. I added an explicit void path gated on the game actually being final with boxscores ingested.

Then the void path over-fired. A missed morning run made 79 real legs look unsettleable and voided them. I caught it, tightened the condition — postponed games never post as Final, so they need an age-based rule instead — and shipped a repair path that flips false voids back once stats arrive. It recovered 73 of the 79.

That second one is the tell. The cheap move was to leave 79 legs voided; nobody would have known, and the record would have looked marginally cleaner. Rebuilding the rows was strictly worse for the numbers and strictly better for the evidence.

LIMITS

What I got wrong

  • Concentration. 10,821 of 10,928 settled legs are one market: hitter over 0.5 hits, the mainline. That is the most efficiently priced player prop on the board — I had written down before this test that I expected it to be unbeatable — and I spent three months proving it. The strategy I actually believed in was alternate lines, which the free odds tier doesn’t carry. This result is about that market, not about the model in general. I tested what was easy to test rather than what I wanted to know.
  • A threshold I moved mid-test. The ≥1,200-leg re-cut point was added on 2026-07-08, during the test, right after the encouraging window. The original gate (N ≥ 200, CLV > 0) is what actually binds; the later threshold is a researcher degree of freedom and I’m flagging it rather than quietly presenting it as part of the original design.
  • Noisy market prices. Single book, free tier, one poll per game, two-way proportional de-vig. p_market carries real measurement error, which attenuates the horse-race coefficients — the model’s true b is probably somewhat higher than 0.231.
  • Calibration drift went unexamined. Predicted minus observed was ~0.002 in June and ~0.074 by August. I have a hypothesis — the simulator treats plate appearances as conditionally independent, so it understates game-level variance — and a partial fix that is implemented but disabled pending a proper variance fit. Unresolved.