A backtesting engine that answers one question with data instead of opinion: can any number-picking strategy beat a lottery?
I built this to settle it for myself. The answer — demonstrated across 2,525 real Mega Millions draws spanning 24 years — is no. But the interesting part is how you prove a negative rigorously, and what you build along the way: the same architecture quantitative trading firms use to backtest market strategies, pointed at a domain where the honest finding is that there's no signal to find.
Pure Python standard library. No dependencies. No pip installs.
1. The machines are fair. Chi-square goodness-of-fit tests (the same statistical audit used on real lottery equipment) across all five Mega Millions rule eras since 2002: every era consistent with uniform randomness. Rolling-window scans, weekday-session splits, repeat-rate analysis, and draw-sum drift all came back clean.
2. No strategy escapes the random band. Seven strategies — hot numbers, cold numbers, overdue, frequency-weighted, repeat-last-draw, popularity-avoidance, and a pure-random control — each played every draw of the 5/70+25 era (766 scored draws, 25 trials each, real prize table, $5/ticket):
strategy main hits/draw bonus hits $ return vs random band
--------------------------------------------------------------------------
hot 0.3649±0.005 4.20% 27.2%±18.1% +0.8 sd
unpopular 0.3407±0.018 4.12% 20.3%±12.3% +0.1 sd
random 0.3549±0.021 3.70% 19.2%±10.4% +0.0 sd
weighted 0.3637±0.018 4.01% 18.3%± 9.9% -0.1 sd
cold 0.3690±0.007 4.28% 17.6%± 3.6% -0.2 sd
overdue 0.3714±0.004 4.13% 16.9%± 3.7% -0.2 sd
repeat 0.3590±0.000 3.13% 12.8%± 0.0% -0.6 sd
Nothing cleared even ±1 standard deviation of the random control. And across two independent eras the rankings reshuffled completely — the signature of noise, not mechanism. (A real edge persists; luck rotates.)
3. The instrument can detect a real edge — that's what makes the negative meaningful. The test suite includes a rigged game where numbers 1–15 are secretly favored. The backtester catches it instantly: the hot strategy surges to 1.32 hits/draw vs. random's 0.35, screaming outside the band. A detector that has proven it can fire, and doesn't, is evidence. A detector that can't fire is decoration.
4. Expected value is the only lens that ever worked — and it's closed. Jerry Selbee's famous edge wasn't number prediction; it was EV arithmetic on a roll-down rule. This repo's EV engine computes exact combinatorial odds for today's Mega Millions (1 in 290,472,336, verified to the digit) and shows the game only crosses break-even on paper above a $1.12 billion cash jackpot — precisely when ticket-sale surges and jackpot-splitting (modeled here with a Poisson co-winner distribution) claw it back below.
Requires Python 3.10+. Nothing to install.
python3 run_phase1.py mega all # era-by-era stats + fairness audits
python3 run_phase2.py --jackpot 300000000 --tickets 80000000 # EV + bias watcher
python3 run_phase3.py --era previous --trials 25 # the backtest verdictFirst run downloads the full draw history (~300 KB) from NY Open Data and caches it in data/. Powerball is supported too (--game power).
Run the offline test suites (no network needed):
python3 test_pipeline.py && python3 test_phase2.py && python3 test_phase3.pylotto_bench/
├── run_phase1.py # stats & fairness reports
├── run_phase2.py # EV engine + pattern watcher
├── run_phase3.py # strategy backtester
├── test_*.py # offline test suites (incl. rigged-game detection)
└── lotto_bench/
├── config.py # game + ERA definitions (5 rule changes since 2002)
├── data_loader.py # download → parse → validate → era-tag
├── stats.py # frequency w/ baselines, gaps, chi-square
├── ev.py # exact odds, EV, break-even, jackpot-split model
├── watcher.py # rolling bias scans, weekday/repeat/sum surveillance
├── strategies.py # 7 pluggable strategies, one shared interface
└── backtest.py # walk-forward scoring vs. the random control band
Design decisions that matter:
- Era-aware everything. Mega Millions has changed its number matrix five times. Mixing eras silently corrupts every statistic (the number 60 didn't exist before 2013 — of course it looks "cold" all-time). Every draw is tagged with its era; no stat ever crosses a boundary. Most lottery-analysis sites get this wrong.
- Baselines, not raw counts. "42 was drawn 19 times" is meaningless until it sits next to "expected: 9.9 ± chance spread."
- No peeking by construction. Strategies receive only the history before the draw they're predicting; the backtest loop never hands them the answer.
- Loud validation. Malformed source rows are reported, never silently dropped.
- stdlib only. Chi-square critical values via Wilson–Hilferty approximation instead of a scipy dependency; hypergeometric odds via
math.comb.
The lottery has no signal — but the process of proving that is transferable to domains that do: walk-forward backtesting, control baselines, detection-floor verification, era/regime segmentation, and expected-value modeling under crowd behavior. The skeleton here is the same one you'd point at A/B tests, pricing experiments, or trading strategies. The lottery was just the domain honest enough to teach the method.
Draw data: New York State Gaming Commission via NY Open Data. This project is research/education; it neither encourages lottery play nor claims any wagering edge — the entire point is that no such edge exists.