A production data contract for Kaggle Polymarket L2

A public dataset is not a research input. It is a pile of files carrying whatever timestamps, units and duplicate rows the recorder happened to produce, under a license that may not permit what you want to do.

A data contract is the layer between the two. It states what the tables guarantee, checks each guarantee rather than assuming it, and records exactly which bytes it read. Without one, an unreproducible result is indistinguishable from a wrong one.

This recipe turns a bounded slice of the Marvingozo Polymarket dataset into canonical, versioned h5i-db tables. The objective is not an alpha claim. It is the evidence layer a credible backtest needs:

Terms used here#

term meaning
data contract the stated guarantees about what a table holds, checked rather than assumed
L2 several order book price levels per side, with the size resting at each
book event one atomic update to the order book, applied in full or not at all
timestamp repair correcting times that arrive in the wrong unit, order, or zone
source hash the checksum of each input file, recorded so the ingest is traceable
YES contract the binary contract that pays 1.00 if the event happens and 0.00 if it does not
snapshot a named, checksummed pin of table versions, taken before any strategy exists

New to any of these? GLOSSARY.md defines them at more length, along with every other term the cookbook uses.

Download only what the recipe uses#

The four files total roughly 165 MB compressed. We intentionally do not download a 1–2 GB daily tick file. Run the printed commands from the repository root if the cache is empty. Kaggle authentication is required.

The dataset is CC BY-NC 4.0: this recipe and its derived work are for non-commercial/academic use unless you obtain separate rights.

from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd

import h5i_db
import cookbook_utils as cu

CACHE = Path("data/cache/kaggle-polymarket")
missing = cu.kaggle_missing_files(CACHE)
if missing:
    commands = cu.kaggle_download_commands(CACHE)
    raise FileNotFoundError("Run these bounded downloads:\n" + "\n".join(commands))
print(f"dataset={cu.KAGGLE_POLYMARKET_DATASET}")
print(f"license={cu.KAGGLE_POLYMARKET_LICENSE}")
output
dataset=marvingozo/polymarket-tick-level-orderbook-dataset
license=CC BY-NC 4.0

Hashing the inputs is part of reproducibility. A Kaggle slug and filename are not immutable identifiers; a publisher can replace a file in a new dataset version.

manifest = cu.kaggle_source_manifest(CACHE)
manifest.to_pandas()[["remote_path", "bytes", "sha256", "license"]]
output
remote_path bytes sha256 license
0 features/ml_features_1m_v2.parquet 32260516 2d0a18fea3a6de1d3fda02c21eec4d1a1ee77dfe900445... CC BY-NC 4.0
1 labels/market_targets.parquet 15356673 901fb2fcb1a69dbf1e898d032e58b21d2f1147ee22ea1b... CC BY-NC 4.0
2 labels/trades.parquet 34051625 244a33af8615f43690fa8def1efa41b300be0edb2f080f... CC BY-NC 4.0
3 snapshots/snapshots_2026-03-09.parquet 84170155 806d49605b59334aebad7770a7c8e4bc5a51e36c8b7e94... CC BY-NC 4.0

Predicate-push down before materialising#

The loader lazily intersects one day of snapshots, feature bars, trades, and closed-market labels. It selects the overlapping market with the most prints, then reads only that market. Ten levels per side are retained.

The current feature artifact spans March 6–11 even though the dataset README describes 21-day coverage. The loader therefore verifies overlap from Parquet metadata and values instead of trusting the prose.

sample = cu.load_kaggle_sample(CACHE, depth_levels=10)
print(sample.question)
print(f"market={sample.market_id}")
print(f"eventual resolution target={sample.target} (audit only, never a feature)")
pd.Series(sample.audit, name="value").to_frame()
output
Will global temperature increase by between 1.10ºC and 1.14ºC in February 2026?
market=0x7d4e693277c1d7990a5227146ee70f56750c73be6f9d129e8558be936848dc85
eventual resolution target=0 (audit only, never a feature)
value
dataset marvingozo/polymarket-tick-level-orderbook-dat...
license CC BY-NC 4.0
sample_date 2026-03-09
source_snapshot_rows 227
source_feature_rows 1390
source_trade_rows 198
canonical_book_rows 4540
canonical_trade_rows 198
depth_levels 10
trade_aggressor_known False
clock_adjustments 227
crossed_snapshots_dropped 0

Validate the causal and book contracts#

timestamp_created_at is later than timestamp_received in this snapshot file, so the two source clocks cannot be interpreted literally as event and arrival time. The loader preserves event time and conservatively sets ts_init = max(created, received). Every correction is counted.

The dataset's bundled target column is deliberately removed from the feature table. Keeping it would leak the eventual market resolution into every training row.

book = sample.book_deltas.to_pandas()
features = sample.features.to_pandas()

assert "target" not in features.columns
assert (book["ts_init"] >= book["ts_event"]).all()
assert sample.audit["crossed_snapshots_dropped"] == 0
assert book.groupby("event_index")["is_last"].sum().eq(1).all()
assert book["price"].between(0.0, 1.0, inclusive="both").all()
assert (book["size"] > 0).all()
print(
    f"{book['event_index'].nunique():,} atomic snapshots, "
    f"{len(book):,} canonical levels, "
    f"{sample.audit['clock_adjustments']:,} clock repairs"
)
output
227 atomic snapshots, 4,540 canonical levels, 227 clock repairs

A top-of-book chart is a fast visual contract test. Crosses, inverted sides, or discontinuous timestamps are much easier to spot here than in a strategy tearsheet.

bids = (
    book[book["side"] == "buy"]
    .groupby(["event_index", "ts_init"], as_index=False)["price"]
    .max()
    .rename(columns={"price": "bid"})
)
asks = (
    book[book["side"] == "sell"]
    .groupby(["event_index", "ts_init"], as_index=False)["price"]
    .min()
    .rename(columns={"price": "ask"})
)
tops = bids.merge(asks, on=["event_index", "ts_init"], validate="one_to_one")
assert (tops["bid"] < tops["ask"]).all()

fig, ax = plt.subplots(figsize=(10, 4))
ax.step(tops["ts_init"], tops["bid"], where="post", label="best bid")
ax.step(tops["ts_init"], tops["ask"], where="post", label="best ask")
ax.set(title="Polymarket YES top of book — bounded Kaggle sample", ylabel="price")
ax.legend()
fig.tight_layout()
output
output figure

Commit the approved research cut#

Raw Parquets remain disposable cache files. Canonical tables and the external-byte manifest become versioned h5i-db inputs. The resolution label is not ingested into the strategy feature table.

db = h5i_db.Database(cu.fresh_db("04_kaggle_polymarket_contract"), create=True)
tables = {
    "instruments": (sample.instruments, "ts_init"),
    "book_deltas": (sample.book_deltas, "ts_init"),
    "trades": (sample.trades, "ts_init"),
    "features_1m": (sample.features, "ts_init"),
}
for name, (table, time_column) in tables.items():
    db.create_table(name, table.schema, time_column=time_column)
    db.append(name, table, note="bounded CC BY-NC Kaggle Polymarket sample")

db.snapshot(
    "kaggle-polymarket-2026-03-09",
    tables=list(tables),
    note="Validated one-market research cut; source hashes recorded above",
)
db.tables()
output
['book_deltas', 'features_1m', 'instruments', 'trades']

What this cut can and cannot prove#

These full snapshots are suitable for conservative snapshot replay and spread/depth studies. They are not the tick-by-tick delta stream, so they cannot reveal every queue transition between snapshots. Do not call a queue-position result from this sample “exact.”

For exact queue replay, apply the same contract to one daily orderbook_YYYY-MM-DD.parquet file with a lazy market_id filter. Never call read_parquet on the whole daily file: the publisher estimates roughly 300 million rows and 50 GB of RAM per day.

db.close()