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?

LotteryCortex combines historical statistics, backtests and tracking in one web app. Analysis does not predict draws or increase your chances of winning.

The information on this website is created in part with the help of artificial intelligence and may contain errors or omissions. It is provided for general informational purposes only and does not constitute professional advice. Independently verify important details and consult a qualified professional when necessary. To the extent permitted by applicable law, we accept no liability for loss or damage arising from the use of, or reliance on, this information. This does not affect statutory rights or liabilities that cannot legally be excluded.