Skip to content

About

Open archive of every Italian Lotto draw since 1939 - plain JSON, auto-updated, with a statistics site.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Italian Lotto archive

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

Layout

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

Format

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

Contest numbers in 2020

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

Wheels

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.

Draw days

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.

10eLotto

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:

  1. Take the ten city wheels in alphabetical order, without Nazionale.
  2. 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.
  3. The first 20 are numbers; the next 15 are extra.
  4. gold is the first number drawn on Bari, double_gold the second.
  5. 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.

Using it

# 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*.json
import 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"])

How it updates

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

  1. Conditionally GET the primary source; a 304 means nothing changed.
  2. Parse it and merge any new draws into the archive.
  3. If the primary still lacks a draw that should already have been published, fetch the fallback source and merge that instead.
  4. 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.

Guarantees

  • 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_rewrite run.
  • Nothing invalid is ever committed. Validation runs before the write, not after.

Sources

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.

Statistics site

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.

Formulas

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 numbers
2026-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:

  1. 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. --dirty keeps the sporca formula: every wheel holding a search number then counts towards SearchDrm.
  2. Isotopia — reported when two of the matching wheels hold a search number in the same extraction position. It is recorded, not required.
  3. Retrovisione — each bet is checked against the Rear preceding 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.

The JSON formula format

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

Development

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.

About

Open archive of every Italian Lotto draw since 1939 - plain JSON, auto-updated, with a statistics site.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages