Skip to content

Repository files navigation

🎰 Lotto Bench

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.

TL;DR — The Findings

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.

Quick Start

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 verdict

First 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.py

Architecture

lotto_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.

What I Took Away

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages