thetadatadx
v11.0.1
Published
Native ThetaData SDK for Node.js — powered by Rust via napi-rs
Downloads
2,427
Readme
thetadatadx (Node.js / TypeScript)
Node.js SDK for ThetaData market data. napi-rs bindings over the thetadatadx Rust crate, shipped as pre-built native addons for Linux x64, macOS Apple Silicon, and Windows x64 (no Rust toolchain required on the consumer).
Every call crosses the napi boundary into compiled Rust: gRPC, protobuf, zstd, FIT decoding, and TCP streaming run inside the thetadatadx crate.
Surface coverage: the TypeScript binding exposes all three ThetaData surfaces — MDDS (historical), FPSS (streaming), and FLATFILES (whole-universe daily blobs). Flat files land via
tdx.flatFiles.*()with.toArrowIpc()and.toJson()terminals plus atdx.flatFileToPath(...)raw-bytes helper — see the Flat Files section for the full method list.REST routing escape hatch:
FallbackPolicy.restAlways+Config.withRestFallback+ fouroptionHistory*WithFallbackasync methods onThetaDataDxClientroute the historical-quote endpoints over a locally-running Terminal's REST surface when the caller wants a single transport for every quote-bearing call. See channel pool design for the connection-recovery story.
Install
npm install thetadatadxPre-built binaries are downloaded automatically for your platform. Supported:
- Linux x64 (glibc)
- macOS arm64 (Apple Silicon)
- Windows x64 (MSVC)
Usage
const { ThetaDataDxClient } = require('thetadatadx');
async function main() {
// Connect (requires ThetaData credentials)
const tdx = ThetaDataDxClient.connectFromFile('creds.txt');
// Or: const tdx = ThetaDataDxClient.connect('[email protected]', 'password');
// Historical endpoints return an array of typed tick objects
// (`OhlcTick[]`, `QuoteTick[]`, ...). Index into the array to
// read a per-row field.
const ohlc = tdx.stockHistoryOHLC('AAPL', '20240315', '60000');
console.log(ohlc.length, ohlc[0].close);
// With timeout
const snap = tdx.stockSnapshotQuote(['AAPL', 'MSFT'], null, null, 5000);
// Streaming — primary fluent contract-first API.
// `Contract.stock("AAPL").quote()` returns a typed `Subscription`
// value the polymorphic `client.subscribe(...)` accepts directly,
// matching the documented Rust / Python shape.
const stock = ContractRef.stock('AAPL');
const option = ContractRef.option('SPY', '20260620', '550', 'C');
await using session = await tdx.streaming((event) => {
if (event.kind === 'quote') {
console.log(event.quote.bid, event.quote.ask);
}
});
tdx.subscribe(stock.quote());
tdx.subscribe(option.trade());
tdx.subscribe(SecType.option().fullTrades());
tdx.subscribeMany([stock.quote(), option.quote()]);
// ...do other work; the callback fires on incoming events...
// `[Symbol.asyncDispose]` runs on scope exit:
// stopStreaming(); await awaitDrain(5000);
// If the drain barrier times out, `console.warn` fires but the
// `using` scope still exits cleanly.
}
main().catch(console.error);The await using form is the recommended API. The StreamingSession
forwards every method call (subscribe, subscribeMany,
unsubscribe, unsubscribeMany, activeSubscriptions,
droppedEventCount, reconnect, ...) to the underlying ThetaDataDxClient
through a Proxy, so adding a new method to the napi binding makes it
callable through the session automatically -- the streaming surface is
a single source of truth rooted in the Rust crate.
For the lower-level escape hatch (e.g. cross-process lifecycle management, custom shutdown ordering), call the lifecycle methods explicitly:
tdx.startStreaming((event) => { /* ... */ });
tdx.subscribe(ContractRef.stock('AAPL').quote());
// ...do other work...
tdx.stopStreaming();
// Drain barrier: by the time `awaitDrain(5000)` resolves, the
// consumer thread is guaranteed to have finished firing the
// callback, so the JS closure can be released without a
// use-after-free race against the LMAX Disruptor consumer.
const drained = await tdx.awaitDrain(5000);
if (!drained) console.warn('drain timed out');Pull-iter delivery — for await (const event of iter) (high-throughput drain)
Push-callback (tdx.streaming(callback) above) is the recommended
default for low-latency single-event reaction. Pull-iter is the
sibling delivery mode for high-throughput batch processing where
the dominant cost is per-event JS work rather than per-event vendor
latency:
const iter = tdx.startStreamingIter();
tdx.subscribe(SecType.option().fullTrades());
for await (const event of iter) {
if (event.kind === 'trade') {
buf.push([event.trade.price, event.trade.size]);
}
}
// Stop the streaming session when done. The async-iterator
// protocol handles `break` cleanly via `return()`, which calls
// `iter.close()` so the worker thread stops blocking on the queue.
tdx.stopStreaming();
await tdx.awaitDrain(5000);The Disruptor consumer pushes events into a per-client bounded
queue; the for await loop drains the queue from the Node main
thread in batches, with the actual queue wait happening on
tokio::task::spawn_blocking so the event loop is never blocked.
Mode is chosen at start. Push and pull are mutually exclusive on a
given client; switch by calling stopStreaming() first. Backpressure
surfaces on the same droppedEventCount() counter as the callback
path.
TypeScript types
Every tick type and FPSS event is emitted as a #[napi(object)] struct on
the Rust side, so the full typed surface lives in index.d.ts
(auto-generated by napi-rs). Import directly from the package:
import type { OhlcTick, GreeksTick, Quote, Trade, FpssEvent } from 'thetadatadx';Historical endpoints return Tick[]. Streaming events arrive through the
startStreaming(callback) registration; the callback receives a
discriminated FpssEvent, narrowed on event.kind:
tdx.startStreaming((event: FpssEvent) => {
switch (event.kind) {
// Market-data ticks
case 'quote': /* event.quote is Quote */ break;
case 'trade': /* event.trade is Trade */ break;
case 'ohlcvc': /* event.ohlcvc is Ohlcvc */ break;
case 'open_interest': /* event.openInterest is OpenInterest */ break;
// Control / lifecycle events — one typed payload per FpssControl variant
case 'login_success': /* event.loginSuccess is LoginSuccess */ break;
case 'contract_assigned': /* event.contractAssigned is ContractAssigned */ break;
case 'req_response': /* event.reqResponse is ReqResponse */ break;
case 'market_open': /* event.marketOpen is MarketOpen */ break;
case 'market_close': /* event.marketClose is MarketClose */ break;
case 'server_error': /* event.serverError is ServerError */ break;
case 'disconnected': /* event.disconnected is Disconnected */ break;
case 'reconnecting': /* event.reconnecting is Reconnecting */ break;
case 'reconnected': /* event.reconnected is Reconnected */ break;
case 'error': /* event.error is Error */ break;
case 'unknown_frame': /* event.unknownFrame is UnknownFrame */ break;
case 'connected': /* event.connected is Connected */ break;
case 'ping': /* event.ping is Ping */ break;
case 'reconnected_server': /* event.reconnectedServer is ReconnectedServer */ break;
case 'restart': /* event.restart is Restart */ break;
case 'unknown_control': /* event.unknownControl is UnknownControl */ break;
}
});Truncated / unrecognised wire frames are filtered before the callback
fires and accounted on the thetadatadx.fpss.decode_failures metric
counter on the Rust side; they never surface as an FpssEvent.
The kind field is typed as a string-literal union narrowed by the
generated index.d.ts — plain strings, not a TS enum (the previous
const enum FpssEventKind was removed in #376 because it broke
downstream consumers with "isolatedModules": true), so it works in
every toolchain including Vite, esbuild, ts-jest, and Next.js.
Each typed control payload mirrors the corresponding FpssControl::*
Rust variant one-for-one — Disconnected.reason / Reconnecting.reason
carry the RemoveReason discriminant as i32, Reconnecting.delayMs is
bigint (u64), Ping.payload and UnknownFrame.payload are
Buffer-backed byte arrays. Both Python and TypeScript surfaces are
generated from fpss_event_schema.toml, so consumer code ports between
the two languages without a discriminator rewrite.
bigint fields
Anywhere a Rust u64 or i64 crosses the napi boundary it surfaces as
JavaScript bigint (not number): volume and count on every
OHLC / EOD tick, droppedEventCount() on the streaming client, and
received_at_ns on every FPSS event. Use bigint literal syntax
(42n) for comparisons or widen to Number(x) at the point of
display (watch for loss of precision beyond 2^53).
Flat Files
Whole-universe daily snapshots over the legacy MDDS port. Decoded schema
is determined at runtime by (SecType, ReqType), so the binding emits
Arrow IPC stream bytes — pair with apache-arrow's tableFromIPC to
materialise a typed Table.
import { ThetaDataDxClient } from "thetadatadx";
import { tableFromIPC } from "apache-arrow"; // peer dep
const tdx = ThetaDataDxClient.connectFromFile("creds.txt");
const rows = tdx.flatFiles.optionQuote("20260428");
console.log(rows.len());
const ipc = rows.toArrowIpc();
const table = tableFromIPC(ipc);
// Or skip Arrow and emit a JSON array of objects
const json = JSON.parse(rows.toJson());
// Generic dispatcher
const oi = tdx.flatFiles.request("OPTION", "OPEN_INTEREST", "20260428");
// Raw vendor CSV / JSONL straight to disk
tdx.flatFileToPath("OPTION", "QUOTE", "20260428",
"/tmp/option-quote", "csv");Available flatFiles.* methods: optionQuote, optionTrade,
optionTradeQuote, optionOhlc, optionOpenInterest, optionEod,
stockQuote, stockTrade, stockTradeQuote, stockEod, plus
request(secType, reqType, date).
Building from source
Only needed if your platform doesn't have a pre-built binary or you want to develop locally:
cd sdks/typescript
npm install
npm run build # requires Rust stable + protocAPI reference
Every historical endpoint from endpoint_surface.toml is exposed as a camelCase method on ThetaDataDxClient. See index.d.ts for the complete method list with JSDoc comments.
