Skip to content
CURRENT
-7 → -6 (+1) over 24 captures ATL @ GB spread -7 → -6 house House backtest last 10: 6–4 · 90-day all-market 55.3% (n=8163) · 20h ago Wire Seahawks plan to have Sam Darnold back at practice Wednesday Wire Cardinals CB Will Johnson (neck) could miss rest of season Wire Rams WR Puka Nacua (hip) inactive for 'MNF' vs. Giants
Access: Anonymous access. Content follows.
how-the-sausage-is-made

A Reproducible Backtest Checklist Bettors Actually Follow

Read the price, role, and market first A 12-point checklist to tell a real edge from a hot streak: test past seasons, price after the juice, charge for the line you miss, prove it holds.

8 sections

Shark Snip Editorial

House byline of the Shark Snip analytics desk — numbers sourced from the data pipeline, not vibes.

Key takeaways (from article sections)

  • The 12-item checklist
  • Why each item earns its slot
  • What a verifiable model package looks like
  • The line you miss: small number, big effect
  • Testing across seasons, in practice
  • Common ways a "winning" backtest is fake
  • What good looks like
  • Run it on your model today

A win rate nobody can check is a story, not a track record. If a model claims it hits 56% on NFL spreads, the right response is "show me exactly which games, which odds, and let me run it again and get the same number." Most touts cannot produce any of that. This is the checklist we follow when we build a model that bets a game for you in Studio and write up how it actually did, and it is what you should demand from anyone selling picks. It is written for a bettor who has a system — or follows one — and wants to know whether the edge is real or just a hot streak.

The 12-item checklist

Print this. Tape it next to your monitor. A backtest is not done until every line is true.

  1. Lock the games and odds. Save the exact dataset of games, scores, and prices you tested on. Anyone re-running gets the same input — or sees instantly that it changed.
  2. Same settings every time. Run it with identical settings so the win rate does not drift between runs. If two runs disagree, you are looking at luck, not a result.
  3. Freeze your dice roll. Models that train have a bit of built-in randomness. Pin it down once and write it in the log so the same model comes out every time.
  4. Pick test seasons by date. Decide which seasons are training and which are the test up front, by calendar — e.g. "learn on 2015 through August 2023, test on the 2024 season." Never let the model train on games it is being graded on.
  5. No peeking at the future. Every input for a game must come from before that game's kickoff, and you should have a check that fails the moment it does not.
  6. Bet real prices. Settle every bet at the actual line you would have gotten (closing or opening — just be consistent), after the juice. -110 wins +0.909 units, a loss costs 1 unit.
  7. Charge yourself for the line you miss. You rarely get the exact number on the screen. Dock a small tax (about a penny on moneylines, a tenth of a point on sides) so the backtest matches reality, not the best-case line.
  8. Test across at least 3 seasons. Use three non-overlapping test windows. A single "2024 season" test is one season of luck dressed up as a system.
  9. Check it knows what it knows. Group your picks by how confident the model was and see if the confidence held up. If the games it called 60% only won 51%, it is overconfident — and betting bigger on "locks" will wreck you.
  10. Report the full scorecard. Win rate, ROI, closing line value, and the honesty scores (Brier, log loss) — not just whichever number looks best.
  11. Save the whole model. The model and everything it needs to make a pick travel together. Half a model makes no picks.
  12. One-button rebuild. A single command that pulls the data, trains, grades the bets, and spits out the numbers. If you cannot reproduce your own win rate with one click, neither can anyone else.

Why each item earns its slot

Locking the data catches silent rewrites

nflfastR refreshes weekly. ESPN's box-score feed has been known to quietly retro-correct snap counts after the fact. If the file has the same name but different numbers than last month, your "re-run" is testing a different set of games. Saving the exact dataset is the receipt that you tested on the same thing.

Split your test seasons by the calendar

If you just shuffle all the games together, the model ends up studying for the test — a Week 17 result helps it "predict" Week 5. Always split by date, and set aside a test season the model never touches while you are tweaking it. If you tuned your bet threshold on the same season you brag about, that season is not really a test.

Three seasons kill the "lucky year" problem

Scoring environments swing season to season — a model that crushes a high-scoring year can fold in a low-scoring one, and the two net out to nothing. But if you only tested on the friendly year you would call it a winner. Testing across three non-overlapping seasons is the minimum to expose this. Five is better.

Confidence honesty beats raw win rate

Two models can both hit 56% straight-up. One calls its winners 65% and its losers 45% — its confidence is honest, the edge is real. The other calls its winners 85% and its losers 15% — it is wildly overconfident, and betting bigger on its "locks" will blow up your bankroll. The honesty scores (Brier, log loss) catch the second one; raw win rate never will. That is why the honesty-score explainer and the confidence-check walkthrough are required reading before you size a real bet.

What a verifiable model package looks like

If you build or download a model, this is everything it should ship with so anyone can check the win rate. Copy it.

nfl-spread-v3/
  README.md           # data SHA, seed, split dates, headline metrics
  requirements.txt    # pinned
  uv.lock
  data/
    games.parquet     # frozen snapshot, SHA listed in README
    games.parquet.sha256
  src/
    features.py       # pure functions, no IO
    train.py
    backtest.py
    settle.py         # -110 payouts, slippage, walk-forward folds
  artifacts/
    weights.bin       # tf.js binary or .pt
    scaler.json       # mean/std per feature, in column order
    feature-order.json
    config.json       # seed, train/val/test dates, model hparams
  reports/
    metrics.json      # brier, logloss, hit-rate, roi, clv per fold
    calibration.png
    fold-pnl.png
  Makefile            # make backtest = single-command rebuild

That package is the receipt. If someone selling picks cannot show you the equivalent, you cannot check their edge — and an edge you cannot check is just a story.

The line you miss: small number, big effect

Docking yourself a tenth of a point on NFL sides looks tiny. It is not. Each half point near the spread is worth roughly 3% — so missing the number by a tenth on average costs about 0.6% ROI per bet. Over 500 plays a season that is a 3-percentage-point haircut, enough to turn a 4% winner into a 1% one. Charging yourself for the line you miss is not pessimism; it is the difference between paper and reality. Tracking closing line value is the real-world test of whether your assumption was honest.

Testing across seasons, in practice

Three test seasons for 2022, 2023, and 2024 — learn on everything before each season, grade on that season:

FOLDS = [
    { 'train_end': '2022-08-31', 'test_start': '2022-09-01', 'test_end': '2023-02-15' },
    { 'train_end': '2023-08-31', 'test_start': '2023-09-01', 'test_end': '2024-02-15' },
    { 'train_end': '2024-08-31', 'test_start': '2024-09-01', 'test_end': '2025-02-15' },
]
for fold in FOLDS:
    train = df[df.kickoff < fold['train_end']]
    test  = df[(df.kickoff >= fold['test_start']) & (df.kickoff <= fold['test_end'])]
    model = fit(train, seed=SEED)
    metrics_for_fold = evaluate(model, test, prices='closing', slippage_pts=0.1)

Average the seasons for your headline number, and show each season too — if the model is +5% one year and -1% the next, that "2% average" is misleading and you owe people the swing.

Common ways a "winning" backtest is fake

  1. "It worked when I ran it." Usually the settings or the dataset quietly changed between runs. Lock both and it holds still.
  2. Bet size tuned on the test season. If you hunted for the "best bet threshold" using your test season's ROI, that ROI is contaminated. Tune on a separate season, lock it, then grade on the test season you never touched.
  3. Juice quietly removed. Some tools default to no-juice payouts and flatter the model. Always show the actual per-bet payout you used.
  4. Mismatched lines. An "opening line" from one site and a "closing line" from another can disagree by half a point or more. Pick one source, write it down.
  5. Cherry-picked best run. If you tried 50 versions and only published the luckiest, that is cheating. Lock one version up front, or report the average and the spread across all of them.

What good looks like

An honest write-up is short and boring. Example:

nfl-spread-v3.2 — learned on 2015 through August 2023, tested on the 2024 season (held out the whole time), across three separate seasons. Hit rate 54.1%, ROI after the juice +2.6%, average closing line value +0.31 pts; honesty scores Brier 0.232 (beating the Vegas-implied 0.241) and log loss 0.661. One command reproduces every number in 4 minutes on a 2020 laptop.

That paragraph beats any "60% lock of the night" pitch — because every claim in it is checkable. The point of the checklist is to make publishing that paragraph easy, and ducking it embarrassing.

Run it on your model today

If you build a model in the browser, Studio already locks the settings and grades your bets the moment you click Backtest, so the win rate comes back the same every time. Use it, test across multiple seasons, and the next time someone challenges your record you have a one-line answer: run it again, watch the same number come back. The bettor desk then tracks your live closing line value against what the backtest promised — that is the loop where a story turns into a track record.

Model calibration from graded predictions

Calibration points render only when a verified source binds prediction probabilities to settled outcomes for the same observations.

Expected value from graded outcomes

Expected-value cells render only when a verified source binds observed win outcomes to the price paid for the same bets.

blog.receipts source receipts_summary · prediction_results window graded receipt window Data as of 2026-09-21T04:04:00.968457+00:00 n=2480
Receiptsas_of 2026-09-21T04:04:00.968457+00:00 · all-market graded rows n=2480
ATS spread win rate50.3% · n=1436 723-713-35 ATS spread
Units · secondary-173.27
Vig-laden ROI · secondary-7.1%
CLV · secondary-11.3%

Frequently asked questions

What makes a sports backtest trustworthy?
Three things: you decided which past seasons are the "test" before you looked at the results, you used the same settings every time so the numbers do not change run to run, and you wrote down exactly which games and odds you tested on. If any of that wobbles, you can get a different win rate from the same model — which means the win rate proves nothing.
How many bets do I need before a backtest is meaningful?
For a flat-staked spread or moneyline model, the wobble in ROI is roughly 95% / sqrt(n) in percentage points per bet. At 500 bets the range around a true 2% edge is ±8 pts — still mostly noise. At 2,000 it tightens to ±4 pts, at 5,000 to ±3 pts. Most bettors mistake a 200-bet hot streak for skill; the math says you cannot tell yet.
Should I include the juice in my backtest payouts?
Yes, always. Test against the real prices you would actually bet (-110 standard, sometimes -105/-115) so the vig you pay live shows up. Pretending there is no juice inflates ROI by roughly 4.5 points on flat -110 markets and 2–3 points on reduced-juice books. Publish both numbers if you want, but the headline figure has to be after the juice.
How do I keep my model from cheating by peeking at the future?
Only let it use information that existed before kickoff for that game. The two ways it sneaks a peek: (a) a "last 5 games" average that accidentally includes the game you are predicting, and (b) full-season stats glued on without a cutoff date. The fix is a check that fails the moment any input for a game is dated on or after that game's kickoff, run every time you rebuild.

Build a free model in 60 seconds →

Go →
8m read time
0 players/teams
8 key angles

Angles in this read

  • Probability bands Ranges and uncertainty are shown as bands rather than fake certainty.
  • Odds tick Micro tick movement reinforces live market and pricing language.
  • Line arrow Spread, total, and price movement sections get directional cues.
  • CLV scan A scanning underline highlights closing-line value concepts.
  • Edge meter Positive expected value is presented as a meter, not a guarantee.
  • Model sparkline Model output and projection movement get a tiny sparkline rhythm.

This article does not name specific players or teams, so its context stays limited to box score, closing line value and model from the post itself.

Terms found in this article
box scoreclosing line valuemodelpricebacktest
Share this guide Help another reader make a sharper decision.

Get picks in your inbox

One email, every slate — ranked edges, no touts. Unsubscribe any time.

Start free — pick a sport

Go →

Continue with evidence

Related reading and source status

Related Reads

Inside the Six-Stage Reveal When Training Finishes — on the Advanced Canvas — Shark Snip
Betting Tools

Inside the Six-Stage Reveal When Training Finishes — on the Advanced Canvas

A finished training run on /build/[slug] plays a fixed six-stage reveal, grade first, timed to the exact millisecond in the source.

Sep 11, 2026 5 min read
The Screen That Shows Your Backend Before Training Starts — on the Advanced Canvas — Shark Snip
Betting Tools

The Screen That Shows Your Backend Before Training Starts — on the Advanced Canvas

Pre-Train Preview on /build/[slug] names the exact backend a run will use, from a single side-effect-free probe, before training starts.

Sep 11, 2026 5 min read
Eight Lattice Slots, and the 56 Modules That Can Fill Them — Shark Snip
Betting Tools

Eight Lattice Slots, and the 56 Modules That Can Fill Them

The /build board holds eight fixed slots filled from a live, growing list of pieces, not a four-tier gallery.

Sep 11, 2026 6 min read

query: loadMergedBlogPostCards + scoreRelated · n = 3

No data

No graded source picks match this article yet

The public.source_accuracy_scores 90-day query returned no rows for this article's inferred sport.