Skip to content

Explore WCWins

Track 2 · Finding and Validating an Edge · Lesson 12 of 24

Backtesting without fooling yourself

Most impressive betting backtests fail for mundane reasons: data that were not available at the time, duplicated rows, too many tries on the same history. Learn point-in-time discipline, multiple-testing corrections and walk-forward validation.

Learning objectives

  • Build point-in-time datasets using as-of timestamp joins and identify look-ahead, duplicate and survivorship errors.
  • Compute the family-wise false-positive rate for k tests and the matching Bonferroni threshold.
  • Run a chronological or walk-forward validation and explain why in-sample results overstate performance.
  • Re-run a flawed backtest after correcting its data and report how the headline changes.

Point-in-time data and timestamp joins

A backtest asks: what would have happened if I had followed this rule, knowing only what I could have known at the time? Every price and every feature therefore needs a timestamp for when it became available, and every join must be an as-of join: take the latest value recorded before the decision time, never a value from later.

quote_time ≤ decision_time < kick-off, feature_time ≤ decision_time
The two conditions every row in a pre-match backtest must satisfy.
  • Look-ahead prices: quotes captured after kick-off, or closing odds used as if available in the morning.
  • Look-ahead features: season averages that include the match being predicted; confirmed line-ups used for a decision made before they were published.
  • Duplicate markets: the same quote stored twice, or alternative lines (−0.5 and −0.75) counted as independent bets on the same outcome.
  • Survivorship: only leagues, books or teams that still exist today; strategies kept because they 'survived' earlier tests.

Multiple testing: why the best result is biased

Test one rule at the 5% level and a skill-free rule passes 5% of the time. Test many and the chance that at least one passes climbs quickly.

FWER = 1 − (1 − α)ᵏ Bonferroni: α_per test = α / k
With α = 0.05 and k = 20 independent tests, FWER = 64.15% and the Bonferroni threshold is 0.0025. With k = 40, FWER = 87.15% and the threshold is 0.00125.

Out-of-sample and walk-forward validation

Split by date, never at random: train on earlier matches, test on later ones, and do not look at the test period until every choice is frozen. Walk-forward validation repeats this: fit on a window, test on the next block, roll forward, and report the stitched-together out-of-sample results.

Three details make walk-forward honest. Refit only with data available at each refit date, including any hyperparameters such as a time-decay rate. Leave a gap between training and test blocks when features use rolling windows, so the last training rows do not share matches with the first test rows. And once you have looked at a test block and changed the model because of it, that block has become training data: you need fresh, untouched data for the next claim.

A small synthetic illustration: on the first 365 days of the lab dataset (600 events) we tried 21 favourite thresholds from 0.30 to 0.80. The best, 0.725, returned −3.14% on 131 bets in sample. On the following 365 days it returned −5.81% on 125 bets. The selected rule looked better than its neighbours mostly because it was selected; with 21 tries, the chance of at least one 5%-level false positive would be 65.94%.

Split the synthetic data by date and compare the market baseline, an overconfident model and a leaky model.

Lab 4 · Calibration and backtesting

Synthetic · deterministic
1,200 synthetic events over two years with a known true probability, a margin-inclusive price and an outcome. Move the train/test date and compare three forecasting models.
Lower is better for every score. ECE = expected calibration error (10 buckets).
ModelTrain BrierTest BrierTest log lossTest ECE
Market baseline0.21000.20390.59410.0386
Overconfident model0.22720.22010.66150.1291
Leaky model0.08870.08610.32060.1936
Leaky model, deployed (no post-event feature)–0.20390.59410.0386
Overconfident, recalibrated (T = 2.10)–0.20370.59360.0254
  • Observed rate
  • Perfect calibration
Reliability diagram on the test periodObserved frequency of the event in each forecast bucket for the overconfident model on test events. A calibrated model follows the dashed diagonal.0%20%40%60%80%100%20%40%60%80%Forecast bucket midpoint (%)Observed rate (%)
Observed frequency of the event in each forecast bucket for the overconfident model on test events. A calibrated model follows the dashed diagonal.
Show data table
Forecast bucket midpoint (%)Observed ratePerfect calibration
5%21%5%
15%24%15%
25%36%25%
35%59%35%
45%47%45%
55%48%55%
65%57%65%
75%59%75%
85%72%85%
95%81%95%
Favourites (≥65%) win rate76.2%
Favourites ROI at offered odds−5.7%
Favourite bets in test181
Fitted temperature2.10
  • Leakage: the leaky model scores far better than the market in the backtest, then matches the market exactly once the post-event feature is unavailable. A backtest is only as honest as its timestamps.
  • Calibration: the overconfident model ranks events like the market but its probabilities are too extreme, so its log loss and ECE are worse. A single temperature fitted on the training period repairs much of it.
  • Train/test separation: switch the fit to “All data” and the test score improves because the test period was used to fit — an optimistic estimate you could never achieve live.
  • Win rate is not profit: backing favourites wins most of the time and still loses money after the margin.

Educational calculator. Uses only the numbers you enter or seeded synthetic data — no live prices, no staking, no real money.

A published profit that was one bad row

Lesson 21 studies that correction in depth. The worked example below reproduces the same failure in miniature with the synthetic rows of the replication lab.

  • Profit concentration: what share of total profit comes from the top one, five and ten bets? If one row dominates, inspect it first.
  • Price sanity: flag any recorded price far from the consensus at the same time (lesson 7); a tenfold gap is almost always an error.
  • Uniqueness: enforce one row per event, market key, selection and quote time.
  • Executability: cap every stake at the limit the book accepted at the time, and drop bets the operator later voided.
  • Pre-registration: write down the rule, the data window and the success test before running the out-of-sample period.

Find the bad price, the look-ahead quotes, the duplicate and the unexecutable stake before reading the worked example.

Lab 5 · Research replication challenge

Synthetic · deterministic
A fictional research note claims a spectacular return. Audit its 27 recorded bets (fictional teams, synthetic data). Flag each row you believe is invalid. Selection rule: bet when the recorded price is at least 7.5% above the consensus price. The 7.5% threshold was chosen after testing 40 thresholds on this same sample.
Claimed ROI (as recorded)+161.9%
Claimed profit501.8 on 310
ROI after your flags+161.9%
Bets after your flags27
Times are UTC. Consensus = median price of the other bookmakers at the same moment. Stakes in units.
IDMatch · pickKickoffQuoted atOddsConsensusStake / maxResultYour flag
B01Harbour City v Northvale · Home08-16 14:0008-16 11:052.101.9410 / 25Won
B02Ashbridge v Millford · Away08-16 14:0008-16 10:403.403.1410 / 25Lost
B03Kestrel Park v Dunmore · Home08-17 13:0008-17 09:151.951.8010 / 25Won
B04Redcastle v Ferrybank · Draw08-23 14:0008-23 12:102.602.4010 / 25Lost
B05Northvale v Glenrock · Away08-23 16:3008-23 13:002.252.0810 / 25Lost
B06Millford v Harbour City · Away08-30 14:0008-30 10:203.102.8710 / 25Won
B07Dunmore v Ashbridge · Home08-30 14:0008-30 11:452.051.8910 / 25Lost
B08Glenrock v Kestrel Park · Away08-31 15:3008-31 12:301.901.7510 / 25Won
B09Ferrybank v Redcastle · Draw09-13 14:0009-13 09:502.802.5910 / 25Lost
B10Harbour City v Dunmore · Home09-13 16:3009-13 14:002.402.2210 / 25Won
B11Ashbridge v Glenrock · Away09-20 14:0009-20 10:103.603.3310 / 25Lost
B12Millford v Northvale · Home09-20 14:0009-20 11:302.001.8510 / 25Lost
B13Redcastle v Kestrel Park · Away09-27 14:0009-27 09:402.302.1210 / 25Won
B14Dunmore v Ferrybank · Draw09-27 16:3009-27 13:202.752.5410 / 25Lost
B15Glenrock v Harbour City · Home10-04 14:0010-04 10:001.981.8310 / 25Won
B16Northvale v Ashbridge · Away10-04 14:0010-04 11:153.253.0010 / 25Lost
B17Kestrel Park v Millford · Home10-18 14:0010-18 10:252.151.9910 / 25Lost
B18Ferrybank v Glenrock · Away10-18 16:3010-18 12:502.502.3110 / 25Won
B19Harbour City v Redcastle · Home10-25 14:0010-25 09:302.202.0310 / 25Lost
B20Ashbridge v Dunmore · Draw10-25 14:0010-25 11:403.002.7710 / 25Lost
B21Millford v Kestrel Park · Away11-01 14:0011-01 10:052.452.2610 / 25Lost
B22Northvale v Ferrybank · Home11-01 16:3011-01 13:101.921.7710 / 25Lost
B23Kestrel Park v Ashbridge · Away11-08 14:0011-08 10:3041.004.1010 / 25Won
B24Redcastle v Northvale · Home11-08 14:0011-08 14:172.902.1210 / 25Won
B25Glenrock v Millford · Draw11-15 14:0011-15 16:052.702.4910 / 25Won
B26Millford v Harbour City · Away08-30 14:0008-30 10:203.102.8710 / 25Won
B27Dunmore v Glenrock · Home11-22 14:0011-22 12:002.652.4550 / 25Won
Is the 7.5% threshold evidence of an edge?

Educational calculator. Uses only the numbers you enter or seeded synthetic data — no live prices, no staking, no real money.

Worked example

Worked example · Cleaning a 161.87% backtest

The Research Replication Challenge holds 27 synthetic bets (fictional teams) selected by the rule 'bet when the recorded price is at least 7.5% above the consensus'. The 7.5% threshold was chosen after testing 40 thresholds on the same sample. Stakes are 10 units except one.

  1. Headline resultAll 27 recorded rows27 bets, 13 wins, 310 staked, profit +501.80, ROI +161.87%
  2. Bad price (B23)Recorded 41.0 against a consensus of 4.10; this one win contributes 10 × 40 = 400 unitsRemove it: 26 bets, profit +101.80, ROI +33.93%
  3. Look-ahead quotes (B24, B25)Quoted 17 minutes and 2 hours 5 minutes after kick-offRemove both: 24 bets, 280 staked, profit +65.80, ROI +23.50%
  4. Duplicate (B26)Same match, selection, time and price as B06Remove it: 23 bets, 270 staked, profit +44.80, ROI +16.59%
  5. Unexecutable stake (B27)Stake 50 against a maximum of 25Cap at 25: 23 bets, 245 staked, profit +3.55, ROI +1.45%
  6. The 22 rows with no planted flawB01–B22 only22 bets, 8 wins, profit −37.70, ROI −17.14%
  7. Multiple testing on the threshold1 − (1 − 0.05)^40 for independent null tests; Bonferroni 0.05 / 40Illustrative FWER 87.15%; shared-data thresholds need not be independent; per-test threshold 0.00125

Each correction is mechanical, yet together they turn +161.87% into +1.45% on 23 bets — and that small positive figure rests on a single 25-unit win (B27); the 22 unflawed rows lost 17.14%. With the threshold chosen from 40 tries, the honest conclusion is no evidence of an edge.

Practice · Watch a leaky model collapse

Open the Calibration and Backtesting lab with the chronological split at day 365. Compare test-period Brier score and log loss for the market baseline, the overconfident model and the leaky model, then switch on point-in-time features.

Lab 4 · Calibration and backtesting

Synthetic · deterministic
1,200 synthetic events over two years with a known true probability, a margin-inclusive price and an outcome. Move the train/test date and compare three forecasting models.
Lower is better for every score. ECE = expected calibration error (10 buckets).
ModelTrain BrierTest BrierTest log lossTest ECE
Market baseline0.21000.20390.59410.0386
Overconfident model0.22720.22010.66150.1291
Leaky model0.08870.08610.32060.1936
Leaky model, deployed (no post-event feature)–0.20390.59410.0386
Overconfident, recalibrated (T = 2.10)–0.20370.59360.0254
  • Observed rate
  • Perfect calibration
Reliability diagram on the test periodObserved frequency of the event in each forecast bucket for the overconfident model on test events. A calibrated model follows the dashed diagonal.0%20%40%60%80%100%20%40%60%80%Forecast bucket midpoint (%)Observed rate (%)
Observed frequency of the event in each forecast bucket for the overconfident model on test events. A calibrated model follows the dashed diagonal.
Show data table
Forecast bucket midpoint (%)Observed ratePerfect calibration
5%21%5%
15%24%15%
25%36%25%
35%59%35%
45%47%45%
55%48%55%
65%57%65%
75%59%75%
85%72%85%
95%81%95%
Favourites (≥65%) win rate76.2%
Favourites ROI at offered odds−5.7%
Favourite bets in test181
Fitted temperature2.10
  • Leakage: the leaky model scores far better than the market in the backtest, then matches the market exactly once the post-event feature is unavailable. A backtest is only as honest as its timestamps.
  • Calibration: the overconfident model ranks events like the market but its probabilities are too extreme, so its log loss and ECE are worse. A single temperature fitted on the training period repairs much of it.
  • Train/test separation: switch the fit to “All data” and the test score improves because the test period was used to fit — an optimistic estimate you could never achieve live.
  • Win rate is not profit: backing favourites wins most of the time and still loses money after the margin.

Educational calculator. Uses only the numbers you enter or seeded synthetic data — no live prices, no staking, no real money.

Show what you should find

The split gives 600 training and 600 test events. On the test period the market baseline scores Brier 0.2039 and log loss 0.5941; the overconfident model is worse (Brier 0.2201, log loss 0.6615); the leaky model looks brilliant (Brier 0.0861, log loss 0.3206) even though its calibration error (0.1936) is far worse than the market's (0.0386). With point-in-time data the leaky model falls back to exactly the market's scores (Brier 0.2039), because its 'skill' was the outcome leaking into the feature.

Common mistakes

  • ✗ Joining prices to matches by date only.Use as-of joins on timestamps: the quote must precede both the decision and kick-off.
  • ✗ Reporting the best of many variants without the count.Report every trial. With 20 tests at 5%, the chance of at least one false positive is 64.15%; Bonferroni requires p < 0.0025.
  • ✗ Random train/test splits for time-ordered matches.Split chronologically or walk forward; random splits leak future information about team strength into training.
  • ✗ Trusting the headline ROI when one row dominates profit.Check profit concentration. One erroneous 41.0 price produced 400 of 501.80 units in the example.

When it fails

Supporting research

Check your understanding

1. You test 20 independent strategy variants at α = 0.05. What is the chance at least one passes by luck alone?
2. A quote was captured 17 minutes after kick-off. How should a pre-match backtest treat it?
3. Why split football data by date rather than at random?

Practice only: saved on this device, no XP and no account record.

Saved on this device only.
Backtesting without fooling yourself · Value Academy · WCWins