Skip to content
Back to guides
Model checks

A Reproducible Backtest Checklist Bettors Actually Follow

Shark Snip Editorial 9 min read

Read the price, role, and market first

A 12-point checklist to tell a real betting edge from a hot streak: test on past seasons, bet real prices after the juice, charge for the line you miss, and prove the win rate holds up.
12 sections
A Reproducible Backtest Checklist Bettors Actually Follow cover art

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 Tinker 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, Tinker 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.

Named modeling examples

A model page is more useful when the feature examples are concrete. Josh Allen rushing attempts, Ja'Marr Chase target share, Nikola Jokic assist rate, Tarik Skubal strikeout projection, Igor Shesterkin starter confirmation, and Islam Makhachev control time are all different prediction problems. A single “player form” feature cannot explain them all, so the model needs sport-specific inputs and review notes.

  • NFL: separate route participation, pressure rate, and red-zone role from box-score volume.
  • NBA: separate usage, minute projection, pace, and back-to-back fatigue.
  • MLB: separate starter skill, handedness, park, weather, and lineup confirmation.
  • NHL and UFC: late confirmations and fight-week news can matter more than a season average.

Model inputs worth naming

Use names as evidence, not decoration. The useful SEO win is that Josh Allen, Ja'Marr Chase, Bijan Robinson and Puka Nacua and Rams, Chiefs, Bills, Eagles and Lions appear inside decisions, thresholds, and internal links instead of being dumped into a keyword list.

  • NFL model: route participation for Ja'Marr Chase, rushing attempts for Josh Allen, pressure rate allowed by the Bengals, and red-zone carry share for Jonathan Taylor should be separate features.
  • NBA model: usage, projected minutes, rest, and pace should move Nikola Jokic or Shai Gilgeous-Alexander props differently than a one-number power rating.
  • MLB model: Tarik Skubal strikeout projection, Coors Field park factor, lineup confirmation, and bullpen rest need their own columns.
  • Review loop: grade entry price, closing price, bet result, and model error separately so lucky results do not hide bad forecasts.

Build or audit the workflow in Tinker and review it with CLV.

Research note board

Use this model-audit board to keep features, validation, and bet sizing from collapsing into one confidence score.

Model layerWhat to inspectExample inputDowngrade when
FeatureWhether the variable maps to the sport and marketJosh Allen role data or closing line value price movementThe feature is a proxy for something you can measure directly
ValidationOut-of-sample error, CLV, calibration, missing dataRams market movement after injury newsWins come without beating the close or improving calibration
SizingBankroll, confidence interval, correlation, market limitCLV exposure compared with related ticketsMultiple bets repeat the same thesis at full stake

Bet responsibly — set limits, never chase losses.

Model calibration: predicted vs observed

Predicted win probability bucket vs the empirical win rate inside that bucket on the test set. Points on the y=x reference line are perfectly calibrated; points below mean the model is overconfident in that bucket.

EV per $100 across win rate × odds grid

Expected value of a $100 stake at each combination of true win rate and market odds. Anywhere the cell is positive you have a long-run profitable bet; the magnitude shows how aggressive Kelly will size it.

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 →
9m read time
28 players/teams
8 key angles
Angles in this read 6 angles

Editorial example layer

Concrete examples make the page useful: tie player and team names to role, price, matchup, and timing so the content reads like analysis instead of glossary filler.
Patrick MahomesJosh AllenLamar JacksonJoe BurrowJalen HurtsJustin HerbertC.J. StroudTua TagovailoaChiefsBillsRavensEaglesLionsBengalsclosing line valuetarget shareair yardsred-zone roleroute participation
A Reproducible Backtest Checklist Bettors Actually Follow data infographic
Chart view of the article's core numbers. Source: inline-lib-modelVsMarketCalibration-reproducible-backtest-checklist.

Get picks in your inbox

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

Start free — pick a sport

Go →

We use cookies for essential site functionality. With your consent, we also use cookies for analytics and performance monitoring. See our Privacy Policy.