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