npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 a tdx.flatFileToPath(...) raw-bytes helper — see the Flat Files section for the full method list.

REST routing escape hatch: FallbackPolicy.restAlways + Config.withRestFallback + four optionHistory*WithFallback async methods on ThetaDataDxClient route 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 thetadatadx

Pre-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 + protoc

API 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.

Docs