Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

[0.1.1] - 2026-07-07

Added

  • Flat files on the standalone MarketDataClient. The market-data-only client now exposes the full flat-file surface — the flat_files() namespace view plus the flatfile_request / flatfile_request_decoded entries and the five per-dataset convenience methods — matching the unified Client method-for-method. Flat files are account-authenticated market data with no streaming leg, so they belong on the market-data handle; a market-data-only workflow no longer needs the unified client to pull whole-universe per-day distributions. Reached the same way on every binding: Rust and C++ MarketDataClient::flat_files(), and the Python and TypeScript market-data clients, all backed by the same flat-file engine as the unified client.

Fixed

  • C++ flat-file view lifetime. The C++ FlatFiles view now co-owns the client handle (shared_ptr) instead of borrowing it, so it stays valid if the originating client is closed or destroyed while the view — or an in-flight call on it — is still alive. The accessor is now plain const (safe on a temporary), matching the market_data() view; the handle is released only once the last owner drops.

0.1.0 - 2026-07-07

The first public release of ThetaDataDx: a terminal-exact, drop-in market-data SDK across Rust, Python, TypeScript, and C++, plus a bundled HTTP + WebSocket server and an MCP server, all over one Rust engine. It connects straight to ThetaData with nothing to install and run locally, and delivers US stock, option, index, and interest-rate data three ways from a single authenticated client: point-in-time history, real-time streaming, and whole-universe flat files.

The SDK ships under per-language package names: thetadatadx-rs (crates.io), thetadatadx-py (PyPI), and thetadatadx-ts (npm, with its platform packages); thetadatadx-cpp is the in-repo CMake/header target; the MCP server is thetadatadx-mcp-server on npm. The import surface is unchanged — use thetadatadx::… in Rust, import thetadatadx in Python, the thetadatadx namespace in C++.

Added

  • One client, three ways to the data. A single authenticated Client (with Python's async companion AsyncClient) exposes point-in-time history under market_data, real-time streaming under stream, and whole-universe flat files on the client directly (client.market_data.stock_history_eod(...), client.stream.subscribe(...)). Single-purpose MarketDataClient and StreamingClient are available on every binding. API-key authentication is supplied inline, read from THETADATA_API_KEY, or loaded from a .env file, and authenticates both channels; email and password authentication is also supported.
  • The bundled server speaks the ThetaData v3 terminal contract, 1:1. The HTTP server serves the v3 REST and WebSocket surface: v3 {response} bodies, one ISO local-datetime per row, CALL / PUT rights, option rows grouped under {expiration, strike, right}, CSV default with the v3 column order, and plain-text error status. It exposes exactly the terminal's three unauthenticated GET /v3/terminal/* routes (shutdown, fpss/status, mdds/status, one-word channel health) and reports all four connectivity states (CONNECTED / UNVERIFIED / DISCONNECTED / ERROR). REST data endpoints accept format=html; flat-file downloads accept csv (default), json, ndjson / jsonl, and html on the terminal's GET /v3/{sec_type}/flat_file/{req_type} route. On the WebSocket, an option strike defaults to the terminal's 1/10-cent integer (--strike-format dollars switches to a dollar value), and every message header carries the streaming status.
  • MCP server on npm. The thetadatadx-mcp-server server runs with npx -y thetadatadx-mcp-server (no Rust toolchain required; cargo install stays for Rust users), and once authenticated it advertises only the tools the account's per-asset-class subscription grants.
  • History serves a single date or a date range on the same route. Every intraday history endpoint (stock and option ohlc / trade / quote / trade_quote, option open_interest, the intraday greeks and trade-greeks families, and index price) takes an optional date plus optional start_date / end_date: supply date for a single day, or a range on the base route, matching the terminal.
  • Unambiguous field units and semantics. strike means dollars on every typed surface (float / double), with the exact wire integer reachable under the unit-named strike_thousandths (a $550.00 strike is 550000). The option right is the logical character ('C' / 'P'; a uint32_t Unicode scalar in C). EodTick time columns are created_ms_of_day and last_trade_ms_of_day. CalendarDay.is_open is a boolean and status carries the open / early_close / full_close / weekend vocabulary. Absent contract identity is None / undefined (Arrow null) in Python and TypeScript; the C-layout rows carry documented fills with has_contract_id(). Contract.option(...) takes the leg as one named OptionLeg { expiration, strike, right }.
  • Columnar and Arrow across every binding. A pull-based Arrow RecordBatch reader, batches(), delivers decoded streaming events as Arrow record batches on a schedule you control (Rust futures::Stream + .blocking(), Python sync/async iterable over the Arrow C-Data interface, TypeScript AsyncIterable<RecordBatch>, C++ arrow::RecordBatchReader), tunable by batch_size and a linger flush with Block or DropOldest backpressure and a dropped() / ring_dropped() loss signal. Every history result also emits a projected Arrow-IPC frame with exactly the columns the wire carried, drivable from a live call via TypeScript's <method>WithColumns and the C / C++ column-presence out-param; multi-symbol snapshots attribute each row to its own symbol. Python DataFrame conversion releases the GIL and hands off zero-copy over the Arrow PyCapsule interface.
  • Live market value. A per-contract theoretical bid / ask computed from the real-time quote, delivered as StreamData::MarketValue with market_bid / market_ask / integer-midpoint market_price, subscribed via Contract::market_value().
  • Async query surfaces on every binding. TypeScript is fully asynchronous — every network entry point returns a Promise (connect factories, all history methods, the streaming lifecycle, flat files), each endpoint taking its required parameters positionally followed by one optional trailing options object (stockHistoryEOD("AAPL", { startDate, endDate, timeoutMs })). Python AsyncClient gains awaitable constructors and *_async flat-file twins; C++ gains an <endpoint>_async(...) companion returning std::future for every buffered query; and the C ABI and C++ stream market-data results through a tick-chunk callback so peak memory tracks one chunk.
  • Typed, unified error taxonomy. One branded hierarchy across every binding: a ConfigError leaf for environmental faults, InvalidParameterError for rejected configuration, sequence, and flat-file inputs (and, in Python, a subclass of the built-in ValueError), and StreamError for streaming faults. Python adds NotFoundError / DeadlineExceededError / UnavailableError (with NoDataFoundError / TimeoutError aliases). Rate-limit errors expose the decoded server back-off (retry_after / retryAfter / retry_after(); the C-ABI thetadatadx_last_error_retry_after_ms()).
  • Observability and delivery controls. A slow-callback watchdog (slow_callback_count() plus a microsecond threshold setter) counts over-budget callback invocations without ever cancelling one; an optional consumer_cpu knob pins the streaming consumer thread for low-jitter delivery; and epoch-instant accessors (*_timestamp_ms, plus thetadatadx::time::date_ms_to_epoch_ms) compute DST-aware Unix milliseconds on read from any row carrying a date and a milliseconds-of-day column.
  • Robust streaming lifecycle. The reconnect budget resets only after a stable connected window so a flapping connection cannot reconnect forever; reconnect marks the session live only after replay succeeds and prefers the last-known-good host; an in-session server rate-limit or restart signal reaches the reconnect classifier; and delta-decode state resets on reconnect so the first post-reconnect ticks decode against a fresh baseline. Subscriptions are de-duplicated and reference-counted so a duplicate subscribe, or a subscribe racing an in-flight reconnect under command-queue backpressure, stays tracked and replayed exactly once. Read, write, and connect timeouts bound every socket operation, and a panicking I/O thread surfaces as a failure to callback, columnar, and pull consumers alike rather than a false "still streaming".
  • Memory-safe teardown across the bindings. The C++ destructor routes through the drained teardown path (stopping a stream from inside its own callback can no longer read a destroyed callback, and async market-data views are lvalue-only so a call on a dangling temporary is a compile error); the Node.js client never deadlocks on close behind a slow callback nor leaves a silently dead stream after reconnect(); and the Python client runs teardown off the GIL. A retired streaming callback is dropped only after the consumer thread confirms quiescence.
  • Decode and data integrity. Truncated or drifted flat-file and EOD responses fail loud with a typed decode error instead of emitting garbage or zero-filled rows; a wrong-width streaming row is rejected before it poisons the per-contract field cache; market-value arithmetic saturates at the integer extremes instead of panicking on an adversarial quote; OHLCVC volume and count decode as unsigned; projected frames keep the trading date; and OCC-21 parsing and FIT integer runs never panic or drop digits on hostile input.
  • Configuration and connect hygiene. Config invariants run at the single connect funnel every path routes through; the loader rejects a misspelled or unknown section at load time; a server retry hint is clamped to the configured ceiling; the C ABI installs its TLS crypto provider from every connect entrypoint; and the embedded async runtime honors the worker_threads knob. Config sections are market_data and streaming, each channel selected independently (MarketDataEnvironment PROD / STAGE, StreamingEnvironment PROD / DEV) via per-binding setters and the THETADATA_MARKET_DATA_* / THETADATA_STREAMING_* environment variables; an unrecognized or cross-channel value is a hard error naming the key and the valid set.
  • Flat-file formats and parity. FlatFileFormat covers Csv, Jsonl, Json (a single JSON array), and Html (an HTML table) on the SDK surface, restricted to the datasets actually served — option trade_quote / open_interest / eod and stock trade_quote / eod, with an unsupported pair rejected by a typed error before any network round-trip. A machine-checked cross-binding parity contract (parity.toml) covers value-field types and the per-endpoint async and streaming families. A from_file client-construction convenience defaults to the production configuration, list-endpoint results are sorted, and every binding carries generated trade-flag accessors.
  • C ABI and layout. ThetaDataDxContract.strike is double dollars with a trailing int32_t strike_thousandths; per-tick right fields are uint32_t Unicode scalars; and ThetaDataDxCalendarDay.is_open is a C99 bool. The workspace builds and publishes a single thetadatadx artifact — every row carries its price as decoded f64 dollars, and time-and-calendar support is a private internal module.

Security

  • Every client TLS configuration is built with an explicit ring crypto provider and explicit protocol versions rather than a process-global default, so ring is the sole provider in the graph and a connect never depends on an installed default.
  • The streaming login wipes the account password from memory the moment the login frame is sent, so the cleartext password is not retained in released heap or a buffered protocol frame after authentication, on the first connect and every reconnect.
  • Authentication errors carry only the HTTP status and never the upstream response body, the auth client does not follow redirects, and session UUIDs are redacted from Debug output.

Released under the Apache-2.0 License.