Skip to main content

ThetaDataDx

thetadatadx (Python)

The Python SDK for ThetaData market data. Pull US stock, option, index, and rate data three ways — point-in-time history, real-time streaming, and whole-universe flat files — all from a single authenticated client. Connects straight to ThetaData; nothing to install and run locally, no local proxy.

PyPI License Python Discord

Features

  • Complete coverage: stocks, options, indices, and rates across 73 typed endpoints.
  • Three access modes, one client — point-in-time history, real-time streaming, and bulk flat-file downloads.
  • DataFrames built in — every result chains straight to Polars, pandas, or Arrow over a zero-copy boundary.
  • Typed all the way down — every tick is a typed object with attribute access and IDE completion, not a dict.
  • No terminal to run — a direct connection to ThetaData; nothing to install and babysit locally.

Install

pip install thetadatadx-py

pip install thetadatadx-py[polars]   # Polars DataFrames
pip install thetadatadx-py[pandas]   # pandas DataFrames (Arrow-backed)
pip install thetadatadx-py[arrow]    # raw pyarrow.Table
pip install thetadatadx-py[all]      # every optional adapter

Binary wheels ship for Linux, macOS, and Windows and require no Rust toolchain. Wheels use CPython's stable ABI (abi3), so one wheel per platform covers Python 3.12 and up; the free-threaded (3.14t) build runs with the GIL disabled and is selected automatically by pip.

Quick start

from thetadatadx import Client

# Pass your API key directly. Use market_data_type="STAGE" to target staging.
client = Client(api_key="td1_...")

# First-order Greeks for every strike on SPY's 2026-06-18 expiry, as of 2024-03-15
greeks = client.market_data.option_history_greeks_first_order("SPY", "20260618", date="20240315")

df = greeks.to_polars()
print(df.select(["strike", "right", "delta", "theta", "vega"]).head())

Other ways to construct the client:

from thetadatadx import Client, Credentials, Config

# API key from the THETADATA_API_KEY environment variable, or from a .env file
client = Client.from_env()
client = Client.from_dotenv(".env")

# Email and password, inline
client = Client(email="you@example.com", password="your_password")

# Full control: build a typed Credentials + Config (custom hosts, timeouts)
client = Client(Credentials.from_file("creds.txt"), Config.production())

Every market-data method returns a typed list — iterate it, index it, or convert it to a dataframe:

eod = client.market_data.stock_history_eod("AAPL", "20240101", "20240301")
for tick in eod:
    print(f"{tick.date}: O={tick.open:.2f} H={tick.high:.2f} "
          f"L={tick.low:.2f} C={tick.close:.2f} V={tick.volume}")

bars = client.market_data.stock_history_ohlc("AAPL", date="20240315", interval="1m")   # 1-minute bars
exps = client.market_data.option_list_expirations("SPY")
strikes = client.market_data.option_list_strikes("SPY", exps[0])

DataFrames

Every result converts directly to a dataframe — no row-by-row iteration:

greeks.to_polars()   # polars.DataFrame
greeks.to_pandas()   # pandas.DataFrame   (pip install thetadatadx-py[pandas])
greeks.to_arrow()    # pyarrow.Table      (zero-copy)
greeks.to_list()     # list[GreeksTick]

The .to_arrow() terminal hands the underlying Arrow buffers to pyarrow over the Arrow C Data Interface — zero-copy at the boundary — so the table plugs straight into DuckDB, polars, cuDF, or Arrow Flight:

import duckdb

table = client.market_data.stock_history_eod("AAPL", "20240101", "20240301").to_arrow()
con = duckdb.connect()
con.register("eod", table)                 # zero-copy into DuckDB
con.sql("SELECT AVG(close) FROM eod").show()

List endpoints (stock_list_symbols, option_list_expirations, …) return a StringList with the same terminals; the single column is named by the endpoint (symbol, expiration, …). Empty results still convert to a zero-row frame with the full typed schema.

For multi-day backfills, stream the response instead of buffering it. Every market-data builder exposes .stream(handler) / .stream_async(handler) alongside the buffered .list() / .list_async() terminals; the handler is called once per chunk with a typed list, and the previous chunk is freed before the next is fetched, so peak memory stays flat regardless of total size:

def on_chunk(ticks):
    for t in ticks:
        ...   # write to Parquet, push to a bus, accumulate stats

(client.market_data.option_history_quote_builder("QQQ", "20260515").date("20260515")
    .interval("tick")
    .strike_range(5)
    .stream(on_chunk))

Streaming

Real-time quotes and trades flow through the same client. Register a callback and match on typed event classes — Trade, Quote, Ohlcvc, OpenInterest for market data, plus one typed class per lifecycle event (Connected, LoginSuccess, Disconnected, Reconnecting, …):

import time
from thetadatadx import Contract, Quote, Trade

def on_event(event):
    match event:
        case Trade(price=px, size=sz, exchange=ex, ms_of_day=ms, sequence=seq, condition=cond, contract=c):
            print(
                f"{c.symbol} {c.expiration} {c.strike} {c.right} trade price={px:.2f} size={sz} "
                f"exchange={ex} ms_of_day={ms} sequence={seq} condition={cond}"
            )
        case Quote(bid=b, ask=a, bid_size=bs, ask_size=asz, bid_exchange=bx, ask_exchange=ax, ms_of_day=ms, contract=c):
            print(
                f"{c.symbol} {c.expiration} {c.strike} {c.right} quote bid={b:.2f} ask={a:.2f} "
                f"bid_size={bs} ask_size={asz} bid_exchange={bx} "
                f"ask_exchange={ax} ms_of_day={ms}"
            )

spy_call = Contract.option("SPY", expiration="20261218", strike="550", right="C")

with client.streaming(on_event) as session:
    session.subscribe_many([spy_call.quote(), spy_call.trade()])
    time.sleep(60)   # park the main thread while events flow into on_event

Build subscriptions with the fluent Contract API and pass them — one at a time or in bulk — to subscribe / subscribe_many. Every subscription is the same typed value, so quotes, trades, open interest, and market value across contracts mix freely in one list:

from thetadatadx import Contract, SecType

stock  = Contract.stock("AAPL")
option = Contract.option("SPY", expiration="20261218", strike="550", right="C")

with client.streaming(on_event) as session:
    session.subscribe(stock.quote())
    session.subscribe_many([option.quote(), option.trade(), option.open_interest(), option.market_value()])

The option constructor is Contract.option(symbol, *, expiration, strike, right) — the leg parameters are keyword-only, so the call site always reads expiration=…, strike=…, right=… and never depends on argument order. Pair it with Contract.stock(symbol) for equities.

Or take a whole-market feed — every option trade across the universe, no per-contract setup. The full-trade feed sends a quote and an OHLC bar before each trade, so add an Ohlcvc case to the callback to handle the bars:

from thetadatadx import Ohlcvc

def on_full_trade(event):
    match event:
        case Ohlcvc(open=o, high=h, low=lo, close=cl, volume=v, contract=c):
            print(
                f"{c.symbol} {c.expiration} {c.strike:g} {c.right} bar "
                f"o={o:.2f} h={h:.2f} l={lo:.2f} c={cl:.2f} volume={v}"
            )
        case _:
            on_event(event)   # reuse the quote/trade handling above

with client.streaming(on_full_trade) as session:
    session.subscribe(SecType.OPTION.full_trades())
    time.sleep(60)   # the callback runs on the streaming thread — keep it fast

Watch feed health from the main thread without touching the callback. The session resolves the client's observability getters directly: millis_since_last_event() is the staleness clock (a steadily growing value is the earliest sign of a wedged link), ring_occupancy() against ring_capacity() shows how close the consumer is to falling behind, and dropped_event_count() is the cumulative tally of events shed on a full ring:

with client.streaming(on_event) as session:
    session.subscribe(SecType.OPTION.full_trades())
    while True:
        time.sleep(5)
        stale_ms = session.millis_since_last_event()   # None until the first frame
        print(
            f"stale={stale_ms}ms "
            f"ring={session.ring_occupancy()}/{session.ring_capacity()} "
            f"dropped={session.dropped_event_count()}"
        )

Prefer columns? client.stream.batches(...) is a sibling to the callback — the same subscriptions, delivered as pyarrow.RecordBatch values under a fixed schema. The reader is iterable (sync, releasing the GIL on the blocking pull) and async-iterable, and closes on context-manager exit:

# `batches(...)` starts the streaming session, so open it first, then subscribe.
with client.stream.batches(batch_size=8192) as batches:
    client.stream.subscribe(Contract.stock("AAPL").trade())
    for batch in batches:        # or: async for batch in batches
        print(batch.num_rows)

Flat files

Whole-universe daily snapshots for one (security type, request type, date) at a time. The decoded schema follows the request type, so flat-file results chain through the same DataFrame terminals as history:

rows = client.flat_files.option_trade_quote(date="20260428")
print(len(rows))
df = rows.to_polars()                       # or .to_pandas() / .to_arrow() / .to_list()

# Generic dispatcher when security type / request type come from config
oi = client.flat_files.request("OPTION", "OPEN_INTEREST", "20260428")

# Or write the raw vendor file straight to disk — no decode, no row materialise
path = client.flatfile_to_path("OPTION", "TRADE_QUOTE", "20260428",
                            "/tmp/option-trade-quote", format="csv")

The flat-file distribution serves a fixed set of datasets: option trade_quote / open_interest / eod, stock trade_quote / eod. Available flat_files.* methods: option_trade_quote, option_open_interest, option_eod, stock_trade_quote, stock_eod, plus request(sec_type, req_type, date). The generic request(...) and flatfile_to_path(...) paths reject an unserved (security, request) pair with a typed invalid-parameter error.

Endpoint coverage

73 typed endpoints across stocks, options, indices, the market calendar, and interest rates, plus real-time streaming.

Category Endpoints Examples
Stock 13 EOD, OHLC, trades, quotes, snapshots, at-time
Option 47 Every stock surface plus every Greeks tier in both the Black-Scholes and binomial families, open interest, contract lists
Index 9 EOD, OHLC, price, snapshots
Calendar 3 Market open/close, holidays, early closes
Interest rate 1 EOD rate history

Every endpoint is a method on Client. The full per-method list with signatures lives in the API reference; Config.dev() and Config.stage() target the non-production environments.

Errors

Every call raises a typed exception under a common ThetaDataError base — AuthenticationError, RateLimitError, NotFoundError, DeadlineExceededError, InvalidParameterError, and the rest — so the same cases are catchable here exactly as they are in every other binding.

Documentation

License

Licensed under the Apache License, Version 2.0.

Metadata

Release files for thetadatadx-py 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for thetadatadx-py 0.6.1
File Size Uploaded Provenance
thetadatadx_py-0.6.1.tar.gz 1.2 MB Without provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for thetadatadx-py 0.6.1
File
thetadatadx_py-0.6.1-cp314-cp314t-manylinux_2_28_x86_64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ x86-64 Details
thetadatadx_py-0.6.1-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
thetadatadx_py-0.6.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 abi3 Linux glibc 2.17+ x86-64 Details
thetadatadx_py-0.6.1-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details

Total release size: 62.1 MB

Release files / thetadatadx_py-0.6.1.tar.gz

Download URL thetadatadx_py-0.6.1.tar.gz
Size 1.2 MB
Tags Source
SHA-256 checksum
How to use checksums
edaff90b41cec01d83cd63434a7aa5b0952864d8dd88d5f12540f0f3483753f4
BLAKE2b-256 checksum
How to use checksums
0424176d1b8615c9176db7fe10940e299084116ae7928cbcd6906b484551da8c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / thetadatadx_py-0.6.1-cp314-cp314t-manylinux_2_28_x86_64.whl

Download URL thetadatadx_py-0.6.1-cp314-cp314t-manylinux_2_28_x86_64.whl
Size 15.2 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
cf7a91f7ecef465eb9cc35f3a53c1a1f398bbbb4086b644d979220a359e90853
BLAKE2b-256 checksum
How to use checksums
07dedf9ee5e98cde926a8a2360c5e166277cdd5f6a01f5adee078bd72ce1a33f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / thetadatadx_py-0.6.1-cp312-abi3-win_amd64.whl

Download URL thetadatadx_py-0.6.1-cp312-abi3-win_amd64.whl
Size 16.4 MB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
0ad310148fb17b5c02ba9ceaa76f87bb67ce30cf7b81e6c5d53219648ab7013e
BLAKE2b-256 checksum
How to use checksums
88206f2768c2f5326490e0eaa78262cd6c1624d7a2caee5970914049510e05f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / thetadatadx_py-0.6.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL thetadatadx_py-0.6.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 15.2 MB
Tags CPython 3.12 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
67aaf135ae15310bd38b4510f18099229bc76f0270e8881d83b807e76de81aa2
BLAKE2b-256 checksum
How to use checksums
3117f059fff83cee44b5454bb0c8b176f324c1cbf250e1bbeb9f3f4a8c93159d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / thetadatadx_py-0.6.1-cp312-abi3-macosx_11_0_arm64.whl

Download URL thetadatadx_py-0.6.1-cp312-abi3-macosx_11_0_arm64.whl
Size 14.1 MB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9bd26eb516d0c39468827f4647bcba1303fdc5fea26c20fa3b7095d7409c2476
BLAKE2b-256 checksum
How to use checksums
5ae83e7ebf3fa752987b078988e9175fd745a24a9150145836cbff0ae8dcbbdf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for thetadatadx-py 0.6.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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