The SDK now ships under per-language package names, starting fresh at
0.1.0:thetadatadx-rs(crates.io),thetadatadx-py(PyPI),thetadatadx-ts(npm), and the in-repothetadatadx-cpp. The API and import surface are unchanged — only the package names and the version differ. Install the packages below.
ThetaDataDx
High-performance market-data SDKs for ThetaData, in Python, TypeScript, C++, and Rust. One Rust engine under all four. 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, with nothing to install and run locally.
A valid ThetaData subscription is required. The SDK authenticates against ThetaData's Nexus API using your account credentials.
Features
- Complete coverage: stocks, options, indices, and rates across 65 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.
- Greeks on demand: first- through third-order Greeks and implied volatility, served straight from the option endpoints.
- The same surface in every language: identical methods and identical typed errors, Python through Rust.
- No terminal to run: a direct connection to ThetaData; nothing to install and babysit locally.
Clients
Three client shapes over the same engine and the same auth. Pick per workload — they are not layers, they are separate entry points:
| Client | Connects to | Use it for |
|---|---|---|
MarketDataClient |
market-data (HTTP) only | history, snapshots, flat files. Never opens the real-time feed; a streaming call raises an error by design. |
StreamingClient |
the real-time feed only | live trades, quotes, OHLCVC. Never touches market-data. |
Client (unified) |
market-data on construct; the feed is lazy | both from one object. The feed is opened only when you call start_streaming, so a market-data-only workflow never touches it. |
from thetadatadx import MarketDataClient, StreamingClient, Client, Credentials, Config
md = MarketDataClient(Credentials.from_file("creds.txt"), Config.production()) # market-data only
stream = StreamingClient(Credentials.from_file("creds.txt"), Config.production()) # real-time feed only
client = Client(api_key="td1_...") # both; feed stays closed until you stream
MarketDataClientandStreamingClientuse independent channels and independent sessions, so you can run them in separate processes or containers with no shared state and nothing stealing the other's session. A market-data worker next to a streaming worker (or aConfig.stage()market-data instance next to aConfig.dev()streaming instance) is a first-class pattern, not a workaround.
The real-time feed is a single live session per account. Run one
StreamingClient(or one unifiedClientthat streams) per account and fan out to your consumers in-process. Opening a second streaming session on the same account takes over the connection and drops the first. Market-data is unaffected: it is per-request, so it runs alongside streaming and across as manyMarketDataClientinstances as you like.
Market-data concurrency is account-wide and enforced server-side (base allowance set by your highest subscription tier; accounts can be boosted above it). Each client's request pool defaults to your tier's allowance — set
max_concurrent_requeststo use a boosted one — and multipleMarketDataClientinstances share the one server-side budget, with extra requests queuing and running in order. The real-time feed requires a paid subscription — FREE accounts get delayed market-data but no streaming.
Install
Install the per-language packages at
0.1.0below —thetadatadx-rs(crates.io),thetadatadx-py(PyPI),thetadatadx-ts(npm).pip install thetadatadx-py # Python npm install thetadatadx-ts # TypeScript / Node.js cargo add thetadatadx-rs # Rust
Point an AI client (Claude Desktop, Cursor, and others) at the MCP server, no install and no Rust toolchain:
{ "command": "npx", "args": ["-y", "thetadatadx-mcp-server"], "env": { "THETADATA_API_KEY": "your_key" } }
C++ ships as a header plus a small implementation file over a prebuilt library (a CMake target wires it up). See the C++ guide.
Quick start
Pass your API key directly to the client and you are one line from a live connection. Generate a key from your ThetaData user portal, then hand it to the client:
Client(api_key="td1_...")in Python,Client.connectWith({ apiKey: "td1_..." })in TypeScript,Client::builder().api_key("td1_...").connect()in Rust and C++. The key can come fromTHETADATA_API_KEY(the env source) or a.envfile instead of an inline literal. Email and password is also supported: passpasswordinline, load acreds.txtfile (email on line 1, password on line 2), or read theTHETADATA_EMAIL/THETADATA_PASSWORDenvironment variables. For full control over hosts and timeouts, build a typedCredentials+Config(see each SDK's "full control" example).
Python
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-19 expiry, as of 2024-03-15
greeks = client.market_data.option_history_greeks_first_order("SPY", "20260619", date="20240315")
df = greeks.to_polars()
print(df.select(["strike", "right", "delta", "gamma", "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())
Stream live quotes and trades through the same client. The callback matches on typed event classes:
import time
from thetadatadx import Contract, MarketValue, 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:g} {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:g} {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}"
)
case MarketValue(market_bid=mb, market_ask=ma, market_price=mp, ms_of_day=ms, contract=c):
print(
f"{c.symbol} {c.expiration} {c.strike:g} {c.right} market_value "
f"bid={mb:.2f} ask={ma:.2f} price={mp:.2f} ms_of_day={ms}"
)
spy_call = Contract.option("SPY", expiration="20260619", strike="550", right="C")
with client.streaming(on_event) as session:
session.subscribe_many([spy_call.quote(), spy_call.trade(), spy_call.market_value()])
time.sleep(60) # park the main thread while events flow into on_event
TypeScript
import { Contract, Client } from 'thetadatadx-ts';
async function main() {
// Pass your API key directly. Add marketDataType: "STAGE" to target staging.
const client = await Client.connectWith({ apiKey: 'td1_...' });
await client.stream.startStreaming((event) => {
if (event.kind === 'trade' && event.trade) {
const { contract, price, size, exchange, msOfDay, sequence, condition } = event.trade;
console.log(
`${contract.symbol} ${contract.expiration} ${contract.strike} ${contract.right} trade price=${price} size=${size} ` +
`exchange=${exchange} ms_of_day=${msOfDay} sequence=${sequence} condition=${condition}`,
);
} else if (event.kind === 'quote' && event.quote) {
const { contract, bid, ask, bidSize, askSize, bidExchange, askExchange, msOfDay } = event.quote;
console.log(
`${contract.symbol} ${contract.expiration} ${contract.strike} ${contract.right} quote bid=${bid} ask=${ask} ` +
`bid_size=${bidSize} ask_size=${askSize} bid_exchange=${bidExchange} ` +
`ask_exchange=${askExchange} ms_of_day=${msOfDay}`,
);
}
});
const leg = { expiration: '20260619', strike: '550', right: 'C' };
client.stream.subscribeMany([
Contract.option('SPY', leg).quote(),
Contract.option('SPY', leg).trade(),
]);
}
await main();
Other ways to construct the client:
import { Client } from 'thetadatadx-ts';
// API key from the THETADATA_API_KEY environment variable, or from a .env file
const fromEnv = await Client.connectWith({ apiKeyFromEnv: true });
const fromDotenv = await Client.connectWith({ apiKeyFromDotenv: '.env' });
// Email and password, inline
const withLogin = await Client.connectWith({ email: 'you@example.com', password: 'your_password' });
// Full control: load a typed credentials file (custom hosts, timeouts via Config)
const fullControl = await Client.connectFromFile('creds.txt');
C++
#include <thetadatadx.hpp>
#include <cstdio>
int main() {
// Pass your API key directly. Add .stage() before .connect() for staging.
auto client = thetadatadx::Client::builder()
.api_key("td1_...")
.connect();
auto greeks = client.market_data().option_history_greeks_first_order("SPY", "20260619", thetadatadx::EndpointRequestOptions{}.with_date("20240315"));
for (const auto& t : greeks) {
std::printf("K=%.2f %c delta=%+.4f gamma=%+.4f\n",
t.strike, t.right, t.delta, t.gamma);
}
}
Rust
[dependencies]
thetadatadx-rs = "0.4.0"
use thetadatadx::Client;
async fn run() -> Result<(), thetadatadx::Error> {
// Pass your API key directly. Add .stage() before .connect() for staging.
let client = Client::builder().api_key("td1_...").connect().await?;
let greeks = client
.market_data()
.option_history_greeks_eod("SPY", "20260619", "20240101", "20240331")
.await?;
for t in greeks.iter().take(5) {
println!("{} K={:.2} {} delta={:+.4}", t.date, t.strike, t.right, t.delta);
}
Ok(())
}
Call the async function from your application's runtime.
DataFrames
Every market-data result is a typed list that 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)
The same .to_polars() / .to_pandas() / .to_arrow() terminals are available
on flat-file results. For multi-day backfills, stream the response in chunks
instead of buffering it. See Request sizing.
Streaming
One connection, one authentication. Market-data queries work immediately; the
streaming transport connects on the first subscription. Subscribe specific
contracts with the fluent Contract API, 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
The callback above is one of two delivery modes. It pushes one typed event at a time for the lowest latency, ideal when you react to each trade or quote. For bulk and analytics,
client.stream.batches(...)delivers the same subscriptions as Apache ArrowRecordBatchvalues under a fixed schema, ready to pull into pandas, Polars, or DuckDB. Open the reader first, since it starts the session, then subscribe. Every binding has both; the streaming guide covers them.
On an involuntary disconnect the client recovers on its own: exponential backoff with jitter, automatic host failover, then a paced re-subscribe of every active contract. Read liveness directly off the stream with
connection_status()and the last-event timestamp; no separate health poll needed.
Endpoint coverage
65 typed endpoints across stocks, options, indices, the market calendar, and interest rates, plus real-time streaming.
| Category | Endpoints | Examples |
|---|---|---|
| Stock | 16 | EOD, OHLC, trades, quotes, snapshots, at-time |
| Option | 36 | Every stock surface plus five Greeks tiers, open interest, contract lists |
| Index | 9 | EOD, OHLC, price, snapshots |
| Calendar | 3 | Market open/close, holidays, early closes |
| Interest rate | 1 | EOD rate history |
The full per-language method list lives in the API Reference.
Errors
Every binding raises the same typed hierarchy, so the same cases are catchable
in any language: AuthenticationError, RateLimitError, NotFoundError,
DeadlineExceededError, InvalidParameterError, and the rest, all under a
common ThetaDataError base.
Repository layout
| Path | Package | Purpose |
|---|---|---|
thetadatadx-rs |
thetadatadx-rs (crates.io) |
The Rust SDK: tick types, decoders, and the network client in one crate |
thetadatadx-py |
thetadatadx-py (PyPI) |
Python package with DataFrame adapters |
thetadatadx-ts |
thetadatadx-ts (npm) |
TypeScript / Node.js package, prebuilt binaries |
thetadatadx-cpp |
header + prebuilt library | C++ wrapper over the C ABI |
thetadatadx-ffi |
release artifacts | C ABI for embedders |
tools/server |
thetadatadx-server |
Local HTTP / WebSocket server |
tools/mcp |
thetadatadx-mcp-server (npm) |
MCP server exposing every market-data endpoint to AI clients |
docs-site |
— | Documentation site (GitHub Pages) |
Documentation
- Documentation site: getting started, API reference, streaming, server, and MCP
- Changelog
Roadmap
See ROADMAP.md for where the project is headed. Up next: a native Go SDK and a self-updating server. The MCP server now runs straight from npm — npx -y thetadatadx-mcp-server.
Contributing
See CONTRIBUTING.md for development setup and the pull-request process. Community discussion happens on the ThetaData Discord.
License
Licensed under the Apache License, Version 2.0. See LICENSE.