Skip to main content

chainmill

Zipped option chains in, queryable store out.

tests PyPI Python License: MIT

Parallel ingest of vendor option-chain archives into SQLite, with resumable builds and a coverage line you can quote. Map: one worker parses one archive. Reduce: the parent folds every frame into one store, on one connection.

pip install chainmill

Thirty seconds

from chainmill import build, ChainQuery

if __name__ == "__main__":                      # required on macOS and Windows
    report = build("raw_archives/", "chains.db", max_workers=8)
    print(report.summary())
60/60 archives ingested (0 skipped, 0 failed) - 120,000 rows, 60 sessions
2024-01-02 to 2024-03-01

Then query it:

with ChainQuery("chains.db") as q:
    chain = q.chain("2024-01-02", symbol="SPY", option_type="P")
    wide  = q.strikes("2024-01-02", 400, 460)
    busy  = q.liquid("2024-01-02", min_volume=500)
    panel = q.daily_summary()

Re-running skips archives already ingested, so an interrupted multi-year load resumes instead of duplicating rows.

When you outgrow SQLite, there is a door out:

with ChainQuery("chains.db") as q:
    q.to_parquet("chains.parquet")                        # one file, streamed
    q.to_parquet("chains/", partition_by_session=True)    # Hive layout, date=YYYY-MM-DD/

Streamed in batches rather than loaded whole, so a multi-year store does not have to fit in memory. Needs pip install chainmill[parquet].

What it handles

  • Vendor drift. Headers matched case-insensitively with aliases (ivimplied_volatility, volumetrade_volume, expiryexpiration). A file genuinely missing required columns is reported by name, never silently coerced.
  • Bad archives. A corrupt or dateless file is recorded as a failure in the report; it does not abort a 1,400-archive build.
  • Multi-member archives. All CSV members concatenated.
  • Session dates parsed from chain_2024-01-02.zip, SPY20240102.zip or x_2024_01_02.zip, or passed explicitly.
  • Coverage reporting. Rows in, archives ingested / skipped / failed, and the session range — the line a result should be quoted with.

Throughput on a four-core laptop: 60 archives / 120,000 rows in 2.0s including index build. Real archives are larger and compression-bound.

Where this fits

If you want Use
A market-data platform with feeds OpenBB
A general dataframe / warehouse layer DuckDB, Polars, Parquet
To turn a folder of vendor zips into something queryable, correctly chainmill

SQLite and pandas, nothing else — pyarrow only if you export. It is an ingest and lookup layer, not an analytics engine and not a distributed system. Beyond one machine, to_parquet() and point a real warehouse at it.

No market data is included — option-chain licences do not permit redistribution.

Why this exists

This replaces five hand-rolled extractors that accumulated in a private research program over two years. Before rewriting anything, each was tested against a synthetic archive:

extractor rows parsed rows persisted
optimized_extractor 0 0
extract_and_index 50 none — map only
robust_extractor 50 50
space_efficient_processor 50 0

Four defects, each producing a plausible-looking run:

The reader outlived its archive. pd.read_csv(handle, chunksize=...) returns a lazy reader. It was created inside with ZipFile(...) / with archive.open(...) and iterated after both closed — so every archive raised I/O operation on closed file, was swallowed by a bare except, and reported zero rows while printing a line that looked like progress. Here the member is read to completion inside the block, with a regression test that touches the frame after the archive has closed.

Workers were bound methods. Submitting self.extract_file to a ProcessPoolExecutor pickles the entire processor to every worker. Each mutated its own copy of the in-memory indexes; those mutations died with the process, and the parent then wrote the empty originals to disk. The map step here is a module-level function of a path, with nothing shared.

The reduce step was distributed. Workers each opened their own SQLite connection and inserted row-by-row in a Python loop — lock contention plus per-row overhead, and the reason one extractor persisted nothing. One writer, executemany, batched transactions, WAL.

Indexes were built before the load, so every insert paid for them. create_indexes() now runs once, after.

Tests

pip install -e ".[test]"
pytest -q

38 tests, no fixtures on disk — archives are synthesised in tmp_path. Includes an equivalence test asserting the parallel and in-process builds produce identical stores, which is what keeps the map step honest about being a pure function.

Licence

MIT. See CHANGELOG.md and CONTRIBUTING.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chainmill-0.1.0.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chainmill-0.1.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file chainmill-0.1.0.tar.gz.

File metadata

  • Download URL: chainmill-0.1.0.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for chainmill-0.1.0.tar.gz
Algorithm Hash digest
SHA256 45639d1d00bd14052dce9905d4de53d37e68a57358246eac065545f24a408cb4
MD5 a263af9ada6c25b133e5987f64588562
BLAKE2b-256 32b8a3fa4527ab976f35460edd37eb65f733f35381836a8820638202c35c7d42

See more details on using hashes here.

Provenance

The following attestation bundles were made for chainmill-0.1.0.tar.gz:

Publisher: publish.yml on charlieyanhx/chainmill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file chainmill-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: chainmill-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for chainmill-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6af9e47ae089bd72a90673bba67f04ba4d88ee2c0c73b65e73f3c8b9f9f96528
MD5 f67cae10d9410b56810eb917cf16358f
BLAKE2b-256 74e3a94180e264a664c504f02f67789f983a7eefc049421ef131547d346f7122

See more details on using hashes here.

Provenance

The following attestation bundles were made for chainmill-0.1.0-py3-none-any.whl:

Publisher: publish.yml on charlieyanhx/chainmill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page