Every Italian Lotto draw since 7 January 1939, as plain JSON, updated automatically by a GitHub Action.
7,411 draws · 77,396 wheel results · 1939-01-07 → 2026-10-31
| Path | What it is |
|---|---|
data/lotto/<year>.json |
One file per calendar year — the source of truth |
data/lotto/all.json |
Every draw in one file, indented |
data/lotto/all.min.json |
Every draw in one file, minified |
data/lotto/latest.json |
The most recent draw only |
data/10elotto/ |
The 10eLotto draw tied to Lotto, same four kinds of file — see 10eLotto |
index.json |
Coverage, per-year counts, and upstream source state of the Lotto archive |
schema/ |
JSON Schema for a draw, a year file, a 10eLotto draw and year file, and a formula listing |
The Lotto files moved from data/ to data/lotto/ in October 2026. Old URLs such as
data/latest.json and data/<year>.json no longer resolve; insert lotto/ after data/.
{
"date": "2026-09-26",
"contest": 155,
"wheels": {
"bari": [22, 29, 63, 55, 38],
"cagliari": [2, 71, 47, 42, 75],
"firenze": [9, 21, 74, 71, 19],
"genova": [87, 52, 82, 72, 26],
"milano": [57, 19, 11, 37, 76],
"napoli": [72, 78, 31, 37, 7],
"palermo": [82, 15, 68, 28, 60],
"roma": [28, 67, 61, 82, 90],
"torino": [67, 78, 56, 18, 59],
"venezia": [84, 66, 75, 76, 43],
"nazionale": [6, 14, 62, 9, 88]
}
}date— draw date (Europe/Rome), ISO 8601.contest— the official concorso number: the 1-based index of the draw within its calendar year, resetting every January.wheels— the five numbers drawn on each wheel, in extraction order (first to fifth). Each is an integer from 1 to 90, and the five within a wheel are always distinct.
No draw was held from 2020-03-24 to 2020-05-02, eighteen draws in all. The archive keeps counting from where it stopped: 2020-03-21 is contest 35, 2020-05-05 is contest 36, and the year ends at 139. The fallback source says the same.
Some published archives count the cancelled draws as well, and give 54 for 2020-05-05 and
157 for 2020-12-31. No official record has been found to settle it, so for the 104 draws
from 2020-05-05 to 2020-12-31 treat contest as unconfirmed; add 18 to get the other
numbering. Dates and numbers are not affected. The same holds for data/10elotto/.
bari, cagliari, firenze, genova, milano, napoli, palermo, roma, torino,
venezia, nazionale.
Only the wheels that actually drew are present, so always check before indexing:
- Cagliari and Genova begin on 1939-07-08.
- Nazionale begins on 2005-05-04.
- Several wartime dates (1943–1946) are missing individual wheels, and a handful of dates between 1948 and 1969 carry a single wheel only.
- From 2005-05-04 onwards, every date has all eleven.
Draws are currently held on Tuesday, Thursday, Friday and Saturday. This has changed over the years — three a week until 2025, and Saturday only for much of the 20th century — so derive the calendar from the data rather than assuming it.
data/10elotto/ holds the evening 10eLotto draw that is tied to Lotto: one per Lotto
draw since 2009-06-11, never the five-minute game. The layout mirrors the Lotto one:
<year>.json, all.json, all.min.json and latest.json.
{
"date": "2026-10-10",
"contest": 163,
"numbers": [5, 11, 15, 16, 17, 20, 21, 23, 30, 31, 32, 38, 50, 53, 59, 66, 68, 74, 75, 82],
"gold": 16,
"double_gold": 38,
"extra": [7, 10, 29, 39, 44, 55, 56, 61, 62, 63, 64, 77, 78, 80, 88]
}date,contest— those of the Lotto draw.numbers— the 20 drawn numbers, ascending.gold— Numero Oro, from 2014-06-26.double_gold— the second number of Doppio Oro, from 2017-10-03.extra— the 15 Extra numbers, ascending, from 2021-10-21.
A key is absent, not null, before the date its option started.
The values are derived, not downloaded. The draw is a function of the Lotto draw:
- Take the ten city wheels in alphabetical order, without Nazionale.
- Walk the first and second number of each wheel, then the third number of every wheel, then the fourth, then the fifth. Skip a number that is already out.
- The first 20 are
numbers; the next 15 areextra. goldis the first number drawn on Bari,double_goldthe second.- When the ten wheels give fewer than 35 distinct numbers (about one draw in fifty), the walk continues on Nazionale, in extraction order.
The rule was checked against the regulator's published results (adm.gov.it) for every
draw from 2024-01-02 to 2026-10-03: 576 of 576 equal.
scripts/check_10elotto.py repeats that check for one draw, the latest by default, or
--year 2024 --contest 208. It only reports; the archive keeps the derived value.
scripts/diecielotto.py writes the files from data/lotto/, with no network access;
--check only compares. .github/workflows/10elotto.yml runs it after every archive
update, in a commit of its own, so a 10eLotto problem never holds back a Lotto draw. A
Lotto draw that still lacks a city wheel has no 10eLotto record until the wheel arrives,
so data/10elotto/latest.json can be one draw behind data/lotto/latest.json.
# The latest draw
curl -s https://raw.githubusercontent.com/pandvan/lotto-archive/main/data/lotto/latest.json
# Every Bari result in 2026
jq '.draws[] | {date, bari: .wheels.bari}' data/lotto/2026.json
# How often has 90 come out on Napoli since 2000?
jq -s 'map(.draws[] | select(.wheels.napoli != null) | .wheels.napoli)
| flatten | map(select(. == 90)) | length' data/lotto/20*.jsonimport json, pathlib
draws = [d for p in sorted(pathlib.Path("data/lotto").glob("[0-9]" * 4 + ".json"))
for d in json.loads(p.read_text())["draws"]]
print(len(draws), draws[-1]["date"]).github/workflows/update.yml runs twice a day and is fully idempotent — a run with
nothing to do costs a single conditional HTTP request and commits nothing.
- Conditionally
GETthe primary source; a304means nothing changed. - Parse it and merge any new draws into the archive.
- If the primary still lacks a draw that should already have been published, fetch the fallback source and merge that instead.
- Validate everything, then commit only the files whose content actually changed.
A draw is only considered overdue from the morning after it was held: upstream publication time is not predictable, and a false alarm is worse than noticing late.
.github/workflows/reconcile.yml runs weekly. It re-reads the fallback source and compares
every row — and every derived contest number — against what is committed, opening an issue
on any disagreement. Since contest is derived from a draw's position within its year, a
missing draw would silently shift every later contest number; this check is what makes that
derivation safe.
- History is never rewritten silently. If an already-published number changes upstream,
the job refuses to commit and fails loudly. Overriding it takes a deliberate
allow_history_rewriterun. - Nothing invalid is ever committed. Validation runs before the write, not after.
| Primary | brightstarlottery.it — zipped tab-separated text |
| Fallback | lottoscientifico.com — zipped dBase III |
Both were compared row by row when this archive was built: they agree on all 77,176
results, and the derived contest numbers match the fallback's own CONCORSO field for all
7,391 dates.
This repository is a mirror of third-party data and is not affiliated with the operators of the Italian Lotto. Its correctness ultimately depends on those upstream sources.
A static site with the statistics people actually look for — ritardi, frequenze, ambi,
cadenze, figure, decine, numeri spia — is generated from the same JSON and published to
GitHub Pages. Nothing about it is committed: scripts/build_site.py rebuilds site/
from data/lotto/, and .github/workflows/pages.yml republishes it whenever the archive
moves.
.venv/bin/python scripts/build_site.py # writes site/
.venv/bin/python scripts/build_site.py --pretty # readable JSON, for inspecting it
python -m http.server -d site 8000 # then open http://localhost:8000| Path | What it is |
|---|---|
web/ |
The pages themselves — hand-written HTML, CSS and JS, no dependencies |
web/10elotto.html |
The 10eLotto page: its draws filtered by the numbers they hold, a statistics placeholder, and the Formule tab on the 20 numbers |
web/banner.svg |
The tile behind the site name in the banner both pages open with |
web/common.js |
What the two pages share: formatting, the element builder, the theme switch |
web/ui.js |
Charts, tables, cards and the draw dialog both pages build their views from |
web/formula.js |
The formula rule ported to the browser, for the Formule tab |
web/formula-view.js |
The Formule tab itself, shared by both pages; on 10eLotto it hides the options about wheels |
scripts/build_site.py |
Copies web/, then writes the computed JSON next to it |
site/ |
Build output: the pages plus data/meta.json, data/wheels/<wheel>.json, data/draws.json and data/10elotto.json |
The Formule tab takes a listing in the JSON formula format,
uploaded from disk and never sent anywhere, and runs it over the last year, the last 5
or 10 years, or the whole archive, optionally hiding the burnt bets (a match goes only when
every bet it had was burnt — --clean is stricter and drops a match for one) or keeping
only the isotopi. For every formula it lists the matches — which wheel
held which search number, whether they are isotopi, every bet and, for a bet the
retrovisione burnt, the number, wheel, date and position that burnt it. Clicking a
match opens that draw as a matrix with the numbers highlighted.
It also says how every clean bet fared: won when all its numbers came out together
on one wheel of the match within the chosen colpi (20 by default), lost, or still
open. Cercato su widens the search to all ten city wheels, or to those and the
Nazionale. Beside every win rate stands the rate chance alone gives for the same bets
over the same wheels and colpi, and only bets whose colpi have all been drawn enter
either. A ranking compares the formulas, a heatmap shows each one's gap from chance
decade by decade (year by year on a short period) in standard errors, and an opened
formula charts its wins colpo by colpo against the expected ones. web/formula.js is a
port of scripts/lotto/listing.py at the default medium scope, and
tests/test_formula_js.py holds the two to the same output (it needs node, and skips
without it).
The Estrazioni section, beside Statistiche and Formule, lists the draws of a year, newest first, on the chosen wheel or on
all of them, kept when every number typed came out on the wheels shown, either as a row per draw or
each draw as a table of wheels by positions; it reads the raw
data/draws.json and computes nothing.
What it computes, per wheel and for the union of all wheels, over the whole archive and over the last 500 and last 100 draws:
| Statistic | Module |
|---|---|
| Tabellone: delay, frequency, record delay, last seen | lotto/tabellone.py |
| Frequency by extraction position | lotto/stats.py |
| Pair (ambo) rankings, by frequency and by delay | lotto/stats.py |
| Cadenza, figura and decina groupings | lotto/stats.py |
| Odd/even, high/low and sum-of-five distributions | lotto/stats.py |
| Follower (numero spia) counts with their standard scores | lotto/stats.py |
| Chi-square test of equiprobability | lotto/stats.py |
| Coverage: draws per year, per weekday, per wheel | lotto/stats.py |
Two rules keep the thing honest, and they are worth stating because this kind of page usually breaks both:
- Every observed figure is published next to its reference value — the frequency expected under equiprobability, the hypergeometric expectation for a five-number draw, the standard score of a follower count. A frequency without its expectation invites the reader to see a pattern in noise.
- The page says plainly that none of it predicts anything. Draws are independent; a number 150 draws late is exactly as likely as one drawn yesterday. The chi-square tab measures whether the archive departs from equiprobability at all. It does not.
The generated JSON uses English keys and carries no prose; every Italian label lives in
the scripts under web/, which are also the only places the lotto vocabulary appears.
A lookup rule over the archive, not a statistic: scripts/formula.py runs the formula
listings of lotto-convergence against it.
A formula says which numbers to look for in one draw, and which bets to play when they are there:
Name=Formula numeri ripetuti (2010)
SearchNum=3 numbers to look for
SearchDrm=2 wheels they must be spread over
Rear=13 draws the retrovisione reaches back
14 1 14 # 43 19 # 43 27 # 43 64
^ numeri di ricerca ^ the bets to play, one per '#'
.venv/bin/python scripts/formula.py --listing formulas/nsla.json # the latest draw
.venv/bin/python scripts/formula.py --listing frm/allor.integrale.2019.frm --scan --since 2026-01-01
.venv/bin/python scripts/formula.py -s "84 68 # 15 19 # 60" -z 3 # one formula, typed out
.venv/bin/python scripts/formula.py --game 10elotto --listing formulas/nsla.json --scan # on the 20 10eLotto numbers2026-09-26 concorso 155 — 1 matched
1. formula 1 84-68 on palermo 68 · venezia 84
ambata 60
ambo 15-19 (out: 15 on palermo 2026-09-25, position 1)
How a formula is applied:
- Found — every numero di ricerca must have come out on exactly one wheel: a
number on two wheels makes the play sporca and drops the formula. Those wheels must
number exactly
SearchDrm.--dirtykeeps the sporca formula: every wheel holding a search number then counts towardsSearchDrm. - Isotopia — reported when two of the matching wheels hold a search number in the same extraction position. It is recorded, not required.
- Retrovisione — each bet is checked against the
Rearpreceding draws. A bet whose numbers already came out there is rejected, with where and when; what is left is the clean play.
--scope chooses how many wheels that last check searches. The subject is always the
bet's numbers over the same Rear draws:
--scope |
Searches | |
|---|---|---|
strict |
every wheel that drew | a number out anywhere burns the bet |
medium |
the wheels of the match | default — what checkRearView does |
loose |
each of those wheels on its own | a bet burnt on one wheel stays playable on the other, and is reported once per wheel |
$ scripts/formula.py --listing frm/allor.integrale.2019.frm --date 2026-09-01 --scope loose
1. formula 35 35-53-80 on milano 53 · roma 35 80
ambo 78-8 on milano (out: 78 on milano 2026-08-25, position 4) · 78-8 on roma
The same bet under --scope medium is simply rejected, and under strict it is rejected
by 8 coming out on palermo — a wheel with no part in the match at all. They nest: strict
rejects at least as much as medium, medium at least as much as loose.
A formula's bets are five lists — ambate, ambi, terni, quaterne, cinquine — and any of them may be empty. A formula with no bets at all is legal: it reports where its numbers landed, and the retrovisione has nothing to check.
| Flag | |
|---|---|
--listing FILE |
a listing to run: .frm, or the JSON form below |
-s/--search |
one formula written inline, "14 1 62 # 43 19 # 27" (commas work) |
-w/--wheels Y |
wheels the search numbers must be spread over, from 1 (2 past five numbers, and so on) to one per number, 11 at most (default: the listing's SearchDrm, 2 with --search) |
-z/--lookback Z |
draws the retrovisione reaches back (default: the listing's Rear, 9 with --search) |
--scope |
strict / medium / loose — wheels the retrovisione searches |
--clean |
keep only matches the retrovisione left untouched |
--colpi N |
also say how each bet fared over the N draws after its match |
--play |
match / tutte / nazionale — wheels --colpi looks for a win on |
--date, --scan, --since, --until |
one draw, or every draw of a range |
--json |
the report as JSON |
--clean is the filter worth reaching for on a long scan: it keeps only the matches
whose bets all survived the retrovisione, which is most of what makes a match
interesting. Over 2026, allor.integrale.2019 matched on 108 draws and exactly one of
them came through clean. A formula that plays no bets has nothing to burn and is kept.
A whole-archive --scan takes about two seconds. Before 2005 only a handful of wheels
drew, so a formula behaves differently there; --since 2005-05-04 is where all eleven
are present.
The same disclaimer as the statistics site applies, and more sharply: draws are independent, so rejecting a bet whose numbers came out recently does not make the remaining bets any likelier. The rule selects; it does not predict.
.frm is positional: the header is four lines in a fixed order, and a bet's kind is
implied by how many numbers it holds. schema/formula.schema.json defines a JSON form
that says both out loud, and scripts/formula.py --listing reads either.
python scripts/convert_formulas.py ../lotto-convergence/frm # a directory, or single files
python scripts/formula.py --listing formulas/nsla.json{
"name": "Formula Allorquando Integrale 1932",
"source": "allor.integrale.2019.frm",
"size": 3,
"wheels": 2,
"lookback": 9,
"formula_count": 180,
"formulas": [
{
"index": 1,
"numbers": [1, 19, 46],
"bets": {
"ambata": [],
"ambo": [[66, 64]],
"terno": [],
"quaterna": [],
"cinquina": [[64, 65, 67, 56, 76]]
}
}
]
}A formula's bets are five lists — ambate, ambi, terni, quaterne, cinquine — and any of
them may be empty; all five keys are always written. size, wheels and lookback
are SearchNum, SearchDrm and Rear under names that say what they do, and source
records the file a listing was converted from.
Converted listings go to formulas/, which is git-ignored: they are someone else's
formulas in another repository's format, not part of this archive. A directory sweep
picks up *.frm and leaves the author's disabled *.frm.no files alone; naming one
converts it anyway.
python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest # tests
.venv/bin/python scripts/validate.py # check the committed archive
.venv/bin/python scripts/update.py --dry-run| Script | Purpose |
|---|---|
scripts/bootstrap.py |
One-off full import; fetches both sources and proves they agree |
scripts/update.py |
Incremental update, primary first |
scripts/reconcile.py |
Cross-check against the fallback |
scripts/validate.py |
Validate the committed archive |
scripts/diecielotto.py |
Derive data/10elotto/ from data/lotto/; --check only compares |
scripts/check_10elotto.py |
Compare one derived 10eLotto draw with the regulator's page |
scripts/build_site.py |
Build the static statistics site into site/ |
scripts/formula.py |
Run the lookup formula, or a listing, over one draw or a range |
scripts/convert_formulas.py |
Convert .frm listings into the JSON formula format |
Useful flags: --dry-run, --force-refetch, --now <iso> (pretend it is another time,
to exercise the staleness logic), --allow-history-rewrite.