Under the hood

How LotteryCortex works

LotteryCortex combines four techniques usually reserved for quants and data scientists. We make them accessible to anyone buying a ticket.

This page describes every algorithm LotteryCortex runs — from data ingest to ticket selection. Everything is open to document: for each step we name the source file in the codebase. Not a miracle cure, but the most completely publicly documented stack for lottery analysis we could build.

The pipeline in 7 steps

Every draw flows through this chain. Per-algorithm details below.

  1. 01

    Data ingest & anomaly detection

    Per lottery we scrape the official archives (direct HTML fetch with Firecrawl fallback). Each new draw goes through detectDrawAnomaly(): shape check, range check, duplicate detect, and a chi-squared comparison against the last 200 draws. Suspicious draws are blocked before they touch history.

  2. 02

    Engine core: six base signals

    engine-core.ts computes six independent scores per number (Bayes, EMA × 3 half-lives, PMI, Markov), normalizes them to ranks, and combines them with tuned weights into one ensemble score.

  3. 03

    Meta-learner & GBM stacking

    The six engine scores + 6 derived features (interactions, recency, hot/cold streaks) feed a per-lottery logistic regression (meta-learner.ts) and a gradient boosted stumps model (gbm.ts). The stacked blender combines both out-of-fold without leakage.

  4. 04

    Ticket generation with anti-clustering

    We sample thousands of candidate tickets from the top-scoring numbers, but reject anything with too much clustering: max 3 from the same decade, parity within historical band, sum within IQR band of real draws.

  5. 05

    Portfolio optimization

    Greedy submodular selection (portfolio-optimizer.ts) + Markowitz min-variance selector (markowitz.ts) pick K tickets that jointly cover the most probability mass with minimum overlap.

  6. 06

    Popularity correction & EV gating

    popularity.ts penalizes 'pretty' combinations (birthdays ≤ 31, consecutive runs, lower half) because on a hit you share. ev-gating computes jackpot-aware EV per €1 stake and outputs 'play' or 'skip' advice.

  7. 07

    Walk-forward backtest & auto-tuning

    Every Sunday a nested k-fold cross-validation (nested-cv.ts) plus grid search over engine weights and half-life presets runs. A Thompson-sampling bandit (bandit.ts) selects the best-performing preset online based on realized ROI.

Index — 20 algorithms

Algorithms in detail

1. Bayesian frequency shrinkage

How often a number has been drawn, corrected for how much data we have.

  • Raw frequency overestimates 'hot' numbers when history is short. We layer a Dirichlet prior over it with automatic alpha estimation (dirichletAlphaAuto, method-of-moments on empirical variance).
  • The result is a per-number posterior probability that shrinks to 1/N (uniform) while history is small, and follows the true frequency once we have enough draws.
p̂(n) = (count(n) + α) / (totalDraws · k + α · N)

src/lib/engine-core.ts · src/lib/advanced-stats.ts

2-4. Time-weighted EMA with multiple half-lives

Recent draws weigh more than old ones — three horizons in parallel.

  • For each draw i from the end we assign weight w_i = 0.5^(i / H). We run three EMAs in parallel with H = 8, 26, and 78 draws (short/medium/long).
  • Short half-life captures 'hot streaks'. Long half-life prevents overreacting to one weird draw. The ensemble blender picks how much weight each horizon gets per lottery — tuned by grid search.
EMA_H(n) = Σ_i  0.5^(i/H) · 1[n ∈ draw_i]

src/lib/engine-core.ts (HALF_LIVES = [8, 26, 78])

5. PMI co-occurrence boost

Which numbers appear together more often than chance would predict?

  • We compute Pointwise Mutual Information: PMI(a,b) = log [P(a,b) / (P(a)·P(b))]. Positive PMI = the pair appears together more than expected if independent.
  • Per candidate number we sum PMI with all numbers from the most recent draw. That gives a 'follow-the-pattern' signal without overfitting: PMI is bounded and clipped to ±3.
score_PMI(n) = Σ_{m ∈ lastDraw}  clip(log[P(n,m) / (P(n)P(m))], ±3)

src/lib/engine-core.ts

6. Markov transition probabilities

From the last draw we estimate the transition probability to the next number.

  • We build an N×N transition matrix T where T[a,b] = P(b in draw_t+1 | a in draw_t), estimated with Laplace smoothing.
  • For the live score we take the mean of T[a, n] over all a in the last draw. Cold numbers don't stay cold forever; this step models finite memory.

src/lib/engine-core.ts

7. Ensemble & tuned weights

The six signals are merged via weighted rank aggregation.

  • Each signal is converted to rank percentiles [0, 1]. Then linearly combined with weights w = (w_bayes, w_ema1, w_ema2, w_ema3, w_pmi, w_markov). Default uniform; per lottery tuned via grid search + ROI validation.
  • Rank aggregation is robust to scale differences between signals and prevents one outlier score from dominating the ensemble.
score(n) = Σ_s  w_s · rank_s(n) / N

src/lib/engine-core.ts · src/lib/engine-tuning.functions.ts

8. Meta-learner (logistic regression, 12 features)

A per-lottery logreg learns the optimal non-linear combination of engine signals.

  • Feature vector (12 dims): the 6 engine ranks + interactions (EMA1·PMI, Bayes·Markov, EMA3·Bayes) + recency + hot streak (appearances in last 10) + cold streak (consecutive absences).
  • Training: L2-regularized logreg with SGD over the full history. Loss = log-loss on label '1 if the number appeared in the next draw'. Backward-compatible: older 9-dim models still work.
P(n in next draw) = σ(w · features(n) + b)

src/lib/meta-learner.ts

9. Gradient Boosted Decision Stumps

Second meta-learner that captures non-linear interactions without a neural net.

  • Mini-LightGBM clone: iterative decision stumps on the residuals. Each stump picks 1 feature + threshold that maximally reduces loss; we add lr · stump.output to the current prediction.
  • Output is JSON-serializable and stored next to the logreg in engine_config.meta_weights.gbm. The stacked blender combines both.

src/lib/gbm.ts

10. Out-of-fold stacking blender

Logreg + GBM are combined without data leakage.

  • We split history into 5 stratified folds. Per fold we train logreg + GBM on 4 folds and predict on the held-out fold. Those OOF predictions become the inputs for a second-layer logreg-meta that learns the optimal weighting.
  • Result: calibratable probabilities without the meta-learner seeing its own training data again.

src/lib/advanced-stats-v3.ts · src/lib/nested-cv.ts

11. Isotonic & Platt calibration

Raw model probabilities are monotonized so they really are probabilities.

  • Platt scaling fits a 1-parameter sigmoid over raw scores on validation data. Isotonic regression (Pool-Adjacent-Violators) is a non-parametric version that only enforces monotonicity.
  • We measure Expected Calibration Error (ECE) on out-of-fold predictions; the calibrator with lowest ECE goes to production.

src/lib/advanced-stats-v2.ts · src/lib/advanced-stats-v3.ts

12. Anti-clustering ticket generator

Candidate tickets are sampled, bad structures rejected.

  • We draw numbers proportional to ensemble score (softmax with temperature τ tuned per lottery). Each candidate is validated:
  • • max 3 from the same decade (anti-clustering),
  • • sum within historical IQR band (no all-low or all-high tickets),
  • • parity split within empirical distribution,
  • • no arithmetic run of ≥4 consecutive numbers.
  • Rejected candidates are resampled until we have N valid tickets.

src/lib/engine-core.ts (generateAntiClusterTickets)

13. Greedy submodular portfolio selection

From thousands of candidates we pick K tickets that jointly cover the most.

  • Problem: choose K tickets that maximize the sum of P(number drawn) across selected tickets, with diminishing returns for overlap.
  • Submodular functions carry a 1 − 1/e ≈ 63% optimality guarantee for the greedy solution. Per iteration we pick the ticket with the largest marginal gain and discount covered numbers by factor 0.4.
f(S ∪ {t}) − f(S) = Σ_{n ∈ t}  remain[n] · (½ if bonus)

src/lib/portfolio-optimizer.ts

14. Markowitz min-variance portfolio

Beyond coverage we minimize mutual correlation between tickets.

  • Correlation between ticket i and j ≈ |overlap| / √(|t_i|·|t_j|). Greedy: always pick the ticket with highest EV minus λ · Σ corr(current, picked).
  • Risk-aversion λ is a tunable parameter (default 1.2). Higher λ = more diversification, lower expected score per ticket but lower variance of total portfolio payout.
util(t) = EV(t) − λ · Σ_{j ∈ picked}  overlap(t, j)

src/lib/markowitz.ts

15. Shared-jackpot popularity correction

A hit on a 'popular' combination is shared with more winners.

  • popularityScore() counts: % numbers ≤ 31 (birthdays), % in lower half, consecutive runs ≥3, arithmetic progressions, all-same-last-digit, crosses/diagonals on the play slip.
  • Based on that we estimate the number of co-winners and apply adjustJackpotForSharing(): EV in the gating step uses the expected payout after sharing, not the gross jackpot.

src/lib/popularity.ts

16. Jackpot-aware EV gating

Advise 'play' only when expected value is positive.

  • EV = Σ_tier P(tier) · payout(tier) − ticketCost. P(tier) is exact from combinatorial odds (binomial coefficients per lottery formula). Payout(tier) comes from PRIZE_TIERS — for the jackpot class replaced by the current jackpot (after popularity sharing).
  • We also compute the break-even jackpot: the amount where EV = 0. Below that the UI shows '⏸ skip'.
  • Kelly criterion determines optional bet sizing for subscribers who plan multiple weeks.
EV = Σ_t  P(t) · payout(t) − cost   |   break-even = (cost − Σ_{t≠jp} P(t)·payout(t)) / P(jp)

src/lib/ev-gating.ts

17. Regime detection (CUSUM + 2-state HMM + particle filter)

Detect whether the draw generator itself has shifted.

  • CUSUM tracks per number a cumulative deviation from expected frequency and alarms when threshold h is crossed.
  • A 2-state HMM (hot/cold) is trained with Baum-Welch on the frequency time series; transitions are Viterbi-decoded.
  • A Sequential Monte Carlo particle filter maintains 200 particles over the hidden regime state and gives a soft probability per draw.
  • On regime shift the short EMA temporarily gets more weight (online adaptation).

src/lib/advanced-stats-v2.ts · src/lib/advanced-stats-v3.ts

18. Level-3 statistical extensions

Conformal prediction, Negative-Binomial gap model, copulas, Shapley, and more.

  • • Split-conformal prediction → bandwidth on meta-probs with guaranteed coverage.
  • • Dirichlet-Multinomial posterior over the full draw (not just per number).
  • • Negative-Binomial gap model for over-dispersion between appearances.
  • • Gaussian copula for pair dependencies PMI misses.
  • • Permutation test per signal (label shuffle) → p-value for 'better than random'.
  • • SHAP attribution per ticket (linear approximation) → explains why this ticket was chosen.
  • • CVaR / Expected Shortfall over portfolio payouts.
  • • Anderson-Darling goodness-of-fit on uniformity.

src/lib/advanced-stats-v2.ts · src/lib/advanced-stats-v3.ts

19. Auto-tuner: nested CV + grid search

Weekly pg_cron revises engine weights and meta-models per lottery.

  • Outer 5-fold stratified CV → out-of-sample ROI estimate. Inner CV → preset selection.
  • Grid search over 6-dim weight simplex (BMA-weighted) + half-life presets (HALF_LIFE_GRID). For each config: simulate K tickets per fold, score with prize tiers, aggregate ROI with 95% bootstrap CI.
  • A new preset is only accepted if CI-low > baseline ROI (no lucky shot).
  • Results go to engine_config (DB) and are immediately live for all subscribers.

src/lib/engine-tuning.functions.ts · src/routes/api/public/hooks/auto-tune.ts

20. Thompson-sampling bandit (online preset selection)

Between auto-tunes a bandit switches live between presets based on current ROI.

  • Per preset we keep a Beta(α, β) posterior: α = wins (ROI > 0), β = losses. At every production pick we sample from each Beta and choose the highest sample.
  • Updates come from the tracker: as soon as a draw is over, the chosen presets get their α or β +1.
  • Result: even without a full re-tune the system converges online to the best preset.
pick = argmax_p  Beta(α_p, β_p).sample()

src/lib/bandit.ts

21. Walk-forward backtest

Every strategy is tested without future knowledge (no leakage).

  • From MIN_HISTORY we iterate draw by draw. Per draw we build the model from ONLY draws before it, generate N tickets, and score against the real draw.
  • We aggregate hits per tier, mean ROI, cumulative P&L, and a random baseline as a control. A strategy 'wins' only if its CI band is above random.

src/lib/backtest.functions.ts

22. Wheeling systems (covering designs)

Mathematical guarantee: given M correct numbers in the pool, always a Y-out-of-X win.

  • We implement full wheels, key wheels, and abbreviated covering designs. For each (pool size, ticket size, guarantee) we pick the combinatorially optimal set.
  • The wheel generator combines with the portfolio optimizer: candidates are wheeled first, then submodularly chosen.

src/lib/wheel.functions.ts

Honestly

Lotteries are and remain games of chance. No analysis can predict a draw. What LotteryCortex does: help you make better decisions about which combinations to play, when to bet more and when to skip a week.

Lottery draws are designed to be random. No algorithm — here or anywhere else — can predict the outcome. What we do measurably: pick better number spreads, avoid popular combinations (so you share less on a hit), and honestly advise skipping when expected value is negative. The auto-tuner validates weekly that our ROI curve sits significantly above random — if that ever stops being true, you'll read it here first.

Why LotteryCortex?

Most lottery tools are 10+ years old, English-only and desktop-first. LotteryCortex is modern, mobile-first and EU-focused — with an AI engine that keeps learning from new draws.