# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.4.0] - 2026-08-08 ### Removed - **`MarketDataClient::bulk_fetch_plan`, and the public `ShardPlan` and `ShardQuery` types.** Bulk-fetch sharding is automatic on the history builders. This Rust-only method handed back the shard plan for you to run the sub-requests yourself; it had no counterpart on the other bindings and exposed the internal planner types on the public API, which kept the sharding strategy from being a free-to-change implementation detail. The automatic sharding is unchanged. This is a breaking change to the Rust API. - **The `[grpc] max_message_size_mb` config-file key.** The inbound gRPC message ceiling is set in bytes via `[market_data] max_message_size`; the megabyte-denominated duplicate that mirrored it is gone. A config file that still sets `max_message_size_mb` now fails to load. This is a breaking change to the configuration file. ### Changed - **The public API surface is tightened to what the SDK exposes for use.** Internal modules that were reachable by path (`auth` beyond `Credentials`, `backoff` beyond `JitterMode`, `util`) and a few internal decode helpers are now crate-private or hidden from the documented surface. The user-facing types stay exported at the crate root exactly as before (`Credentials`, `JitterMode`), so ordinary code is unaffected; only code that reached into the `auth` or `backoff` module path directly switches to those root re-exports. - **Multi-day option-chain pulls now fan the date shard axis out by call/put.** A both-rights chain (`right = "both"`, `strike = "*"`) pulled over a date range shorter than the request pool cut one shard per day and left the rest of the pool idle; it now also splits each date band into a call band and a put band, so more lanes run at once and the pull finishes sooner. A tick chain is served one day per band, so its date bands split by right only while the resulting call and put single-day bands still fit the request pool. The merged rows are exactly the single stream's, in the SDK's canonical chain order (ascending expiration, strike, right). The `ShardBand::Date` band (surfaced only through `Error::PartialShardFetch`) gains a `right` field; code that matches or builds it updates for the new field. - **TypeScript date and time arguments take a wire-format string only, not a JS `Date`.** A calendar date (`startDate`, `endDate`, `expiration`) or an intraday time (`startTime`, `endTime`, `timeOfDay`) was accepted either as the `"YYYYMMDD"` / `"HH:MM:SS"` wire string or as a JS `Date`. A `Date` is an instant, not a calendar day, so the SDK rendered it in UTC and shifted the requested day for a caller in a non-UTC zone (`new Date(2024, 0, 1)` in Central Europe queried `20231231`). These parameters now accept the wire-format string exclusively. This is a breaking change to the TypeScript surface; format a `Date` yourself before passing it. - **The TypeScript TCP-keepalive setters name their argument `secs`, not `ms`.** `setStreamingKeepaliveIdleSecs` and `setStreamingKeepaliveIntervalSecs` store seconds, but the parameter was labeled `ms`, so an editor hint invited a 1000× unit mistake. The label is now `secs`, matching the Python binding; positional callers are unaffected. - **`StreamingClient.streaming()` returns a `StandaloneStreamingSession`.** The standalone streaming client's session was typed as `StreamingSession` (a `Client` and `StreamView`), so `session.marketData`, `session.flatFiles`, and `session.close()` type-checked but were `undefined` at runtime. The standalone session is now its own type extending only `StreamingClient`, so the type surface matches what the object exposes. ### Fixed - **Rejected stream subscriptions now log at `warn` instead of `debug`.** A subscribe the server rejects (`MaxStreamsReached`, `InvalidPerms`, or a generic error) is dropped; recording that only at `debug` left a capped or unentitled stream looking live under a default log subscriber. It now logs at `warn` with the rejection reason, so hitting the concurrent-stream cap or an entitlement limit is visible without lowering the log level. - **A tick option-chain history pull spanning more days than the request pool now shards one band per day.** The server serves a tick option-chain (`strike = "*"` or `expiration = "*"`, no bar interval) only one day at a time. When such a pull covered more days than the pool width, the date-axis fan-out capped the band count at the pool width and produced multi-day bands the server rejects; it now takes one band per day, with bands past the pool width queued. Stock and single-contract tick pulls, bar pulls, and `at_time` chains still band multi-day and cap at the pool width. - **Retry and reconnect backoff no longer overshoots its configured wall-clock envelope.** `RetryPolicy::max_elapsed` — and the FPSS reconnect envelope — bounds when a retry sequence schedules its next attempt, but it was checked only before each backoff sleep, so a long backoff could sleep well past the envelope before the check fired. The backoff is now clipped to the time left and the loop stops scheduling once the envelope is spent. An individual attempt's own duration remains bounded by the per-request deadline (`with_deadline` / `timeout_ms`), not by the retry envelope. - **A stream delivering only unrecognized frames now reconnects instead of going silent.** The FPSS reader skips a frame with an unknown type code to stay aligned, and an unknown frame deliberately does not refresh the data-plane liveness clock. A peer sending nothing but unknown frames kept each read returning before its per-read timeout, so the no-frame reconnect deadline never fired and the session stayed authenticated while delivering no usable events. The read deadline is now enforced in the skip path too: no recognized frame — data, control, or heartbeat — within the read timeout drops the session and reconnects. - **A synchronous market-data call from inside a Python streaming delivery handler fails fast with `HandlerReentrancy`.** The core already rejects a same-client reentrant request when the pool is committed, but the Python binding runs a delivery handler on a blocking-pool thread (async streaming) or inside the runtime's `block_on` (synchronous streaming), where a reentrant synchronous call would deadlock on the held request permit or abort the runtime with a nested `block_on` before reaching the core guard. The binding now detects the re-entry on that thread and raises `HandlerReentrancy` immediately; issue the request outside the handler (for example from a background thread). - **Per-call `timeout_ms = 0` disables the deadline on every endpoint family.** On the Python and TypeScript list endpoints (`option_list_expirations`, `stock_list_symbols`, and the like) a zero `timeout_ms` / `timeoutMs` raced the call against a zero-duration timer and failed immediately, while the history and snapshot endpoints read zero as "no deadline" (matching `with_deadline(Duration::ZERO)`). Zero now means "no per-call deadline" uniformly; any other value runs under the configured request timeout. - **The TypeScript numeric config setters reject an out-of-range value instead of silently wrapping it.** napi decoded these arguments with V8 `ToUint32`, so `setMaxConcurrentRequests(-1)` wrapped to `4294967295` and drove a multi-billion-entry pool allocation, `1.5` truncated to `1`, and `2**32` wrapped to `0`. The setters now take the value as a number and reject a non-finite, negative, fractional, or over-`u32` input with `InvalidParameterError`; the port setters keep their `0..=65535` range check on top. - **A rejected BigInt config value names its actual cause.** A negative or over-`u64` BigInt passed to a `*Ms` config setter reported "magnitude must fit in u64", which is wrong for a negative value — the magnitude fits, the sign does not. The message now names the cause (a negative sign or an over-`u64` magnitude). The error stays a plain `Error`, matching the built-in `OverflowError` the Python binding raises for the same input. - **A C++ index subscription subscribes to the index, not a stock.** The C ABI ignored the subscription's `sec_type` and treated every non-option contract as a stock, so a C++ `Contract::index(...)` request (for example VIX) subscribed to the wrong instrument while still rendering as an index. The C ABI now maps `sec_type` (`STOCK` / `INDEX`, a null or empty value defaulting to stock for compatibility, any other value rejected as `THETADATADX_ERR_INVALID_PARAMETER`) and the C++ wrapper carries it for underlier subscriptions. - **A synchronous C++ streaming handler exception is surfaced, not swallowed.** The shared trampoline caught and discarded every exception a `_stream(...)` handler threw, and the wrapper checked only the FFI return code, so a decode, storage, or application failure in the handler lost a chunk while the call reported success. The trampoline now retains the first exception, stops delivering further chunks, and rethrows it after the drain; an empty handler is rejected before the stream starts. - **Python streaming callbacks are validated when set, not when they first fire.** A non-callable passed to `start_streaming` or assigned to `reconnect_callback` was accepted and only failed later on a delivery thread as an unraisable `TypeError`. Both now raise `InvalidParameterError` synchronously. - **`Contract.option(right=...)` accepts a `Right` enum.** The Python contract builder took `right` as a plain `str`, so passing the SDK's own `Right.CALL` raised a `TypeError`; it now accepts a `Right` (through its `.value`) as well as a string. - **`Client.from_dotenv` keeps the file's environment and host settings when a single channel selector is overridden.** Passing only `market_data_type=` (or `streaming_type=`) reset the streaming environment and any custom hosts to the production defaults before applying the one override, contradicting the documented field-level override. The file is now the base configuration and the selector overrides only the channel it names. - **`AsyncClient` surfaces the StreamView health getters and the flat-file async terminals.** The async proxy's allowlist omitted `is_authenticated`, `millis_since_last_event`, `last_event_received_at_unix_nanos`, `last_connected_addr`, and `batches`, so those valid operations raised `AttributeError`, and every `*_async` name routed to `market_data`, so `flatfile_to_path_async` could not resolve. The health getters resolve like their sibling diagnostics, and a flat-file `*_async` name falls back to the `flat_files` namespace. - **`Stream::batches` rejects a negative linger.** A negative `linger` on the C++ Arrow batch reader silently became an immediate flush; it now throws `InvalidParameterError`, matching the Python and TypeScript bindings. - **A malformed endpoint argument is reported as `THETADATADX_ERR_INVALID_PARAMETER` on the C ABI.** A rejected request-option argument (for example a boolean that is neither `0` nor `1`) set the untyped `THETADATADX_ERR_OTHER`, so the C++ wrapper threw the generic base exception instead of `InvalidParameterError`; it now sets the typed code. - **Documentation corrections.** The TypeScript reconnect callback is documented as receiving a single `{ reason, attempt }` object — the runtime and `.d.ts` type — not the positional `(reason, attempt)` the prose showed. The pull-based Arrow batch reader is documented as opened before subscribing (building the reader starts the session; a subscribe before it errors), and "block" backpressure as preventing queue-side drops rather than "lossless", across Python, TypeScript, and C/C++. The C++ reconnect `user_data` lifetime and the tick-chunk callback's possible worker-thread affinity are stated, the Python `from_env` helpers name the `InvalidParameterError` a missing `THETADATA_API_KEY` actually raises rather than `ConfigError`, and the streaming shutdown docs no longer claim a repeated `void` shutdown returns `-1`. ## [0.3.0] - 2026-07-31 ### Changed - **The request pool now defaults to the account's subscription tier, and the new `market_data.max_concurrent_requests` config field overrides it with no client-side cap.** The SDK previously treated the tier's `2^tier` concurrency (Free 1 / Value 2 / Standard 4 / Pro 8) as a hard client-side ceiling, which also held back accounts whose server-side allowance had been raised above their base tier — an account boosted to 32 concurrent requests was still capped at 8 by its own client. The tier now only seeds the default: leaving `max_concurrent_requests` unset sizes the gRPC channel pool and the request semaphore to the tier's allowance exactly as before, while an explicit value is used verbatim on every binding (`cfg.max_concurrent_requests = 32` in Python, `setMaxConcurrentRequests` in TypeScript, `set_max_concurrent_requests` in C++ and the C ABI, `[market_data] max_concurrent_requests` in the TOML config file), and bulk-fetch sharding fans out across the full resolved pool — a boosted account now shards past the old `2^tier` ceiling. The server remains the enforcement point: requests past the account's real allowance are rejected as `ResourceExhausted` and retried with backoff before surfacing an error. - **Option-chain pulls now shard by `right` (call / put), not by time.** A chain (`strike = "*"`) is a contract cross-product whose cost is the server assembling its contracts, so slicing the requested window by time — the previous behavior — made every band re-enumerate the whole chain. A both-rights **tick** chain now splits into a call half and a put half, each band assembling half the contracts, and the tier's remaining lanes slice each half by time (at Pro width 8, call × 4 time bands + put × 4). The right split rides the intraday time axis, so it applies to tick chains; a bounded-interval chain stays a single stream on one day and fans out across date bands (both rights together) over a multi-day range. Rows come back in the same canonical order as before (grouped by expiration, strike, right; call before put; time-ascending within a contract), so buffered output is unchanged — only the fan-out is smarter. A chain already pinned to one right (a strike wildcard with `right = "call"` / `"put"`) has nothing to divide on this axis and keeps the equal time split. - **Streaming history builders (`*_stream()`) now shard concurrently like the buffered path, and shard sizing cuts the requested time or date range into equal concurrent bands from the request shape.** Under `bulk_fetch = "auto"` (the default), a large chunk-streaming history pull fans out across the same equal bands as its buffered sibling: every band streams as its own concurrent request, and each band's chunks reach the handler as they arrive — every chunk exactly once, with chunks from different bands interleaved in arrival order (use the buffered builder, or `bulk_fetch = "off"`, when the single stream's exact order matters). The split is decided from the request shape alone: a multi-day range cuts into equal date bands, and a single day with an intraday window cuts into equal time bands for a tick pull. A bounded-interval (bar) pull is never split by intraday time, because its bars accumulate from the request's window start (cumulative vwap on OHLC, last-tick carry on quotes and greeks), so a mid-window seam would hand back different bar values than a single stream even when the row count matches; such a pull stays a single stream on one day and still fans out across date bands over a multi-day range, where the server computes each day independently. There is no density-probe request and no per-endpoint tuning, and provably small pulls run as a single stream. - **`MarketDataClient::bulk_fetch_plan` is now `fn(&self, endpoint, query) -> Option` (was `async fn -> Result, Error>`).** The plan is pure computation on the request shape, so there is nothing to await and no error to surface; call sites drop the `.await` and the `Result` handling. This is a breaking change to the Rust API. - **A failed band no longer takes down a large sharded pull.** A buffered `.await` band that dies mid-collection re-fetches from scratch within the standard retry budget — its rows had not reached the caller, so the replay is invisible and duplicate-free — and only a band that spends its whole budget fails the query. On a chunk-streaming `.stream` / `.stream_async` pull, a band that fails terminally no longer cancels its siblings: the surviving bands drain to completion (every one of their chunks reaches the handler), and the call returns the new `Error::PartialShardFetch` naming the failed band window(s) — the start/end of each lost date or time band — so you can re-pull exactly those slices instead of restarting the whole pull. A streaming band that fails before delivering any chunk still retries transparently, a pull where no chunk reached the handler at all still fails with the underlying error like a single stream, and the call deadline still cancels every band at once, so size `with_deadline` / `timeout_ms` to the whole pull. Mapped to `StreamError` / `THETADATADX_ERR_STREAM` on the Python, TypeScript, C++, and C surfaces. - **A market-data request made from inside a streaming delivery handler on the same client fails fast instead of blocking.** A sharded streaming pull holds its request permit(s) until the handler returns, so a blocking market-data call awaited inside the handler on that same client could otherwise wait on permits that only free once the handler completes. When the client's request pool is fully committed such a call now returns the new `Error::HandlerReentrancy` immediately, with guidance to issue the request outside the handler or from a second client; when the pool has headroom it proceeds unchanged, and any request made outside a handler keeps the ordinary wait. ### Added - **Shard-decision logging.** The bulk-fetch planner now logs at `debug` why a pull did NOT shard (endpoint outside the shardable set, no cut axis in the request shape, a bounded interval on the intraday time axis, fan-out width under two, an unparsable band window, a provably small bar grid, or a window too narrow for two bands) next to the existing "sharded" line, and every band's work is tagged with its window: retry warnings carry the band span, and each band emits a completion line with its row count and duration, so concurrent bands stay distinguishable in the log stream. - **Whole-market `at_time` history now shards along the date axis.** `stock_at_time_trade`, `stock_at_time_quote`, and `index_at_time_price` return one row per day and are dominated by per-day server compute rather than transfer, so a multi-day date range now splits into equal concurrent bands like the other bandable history endpoints — cutting a wide as-of pull to a fraction of its single-stream wall time. Buffered output is byte-identical to the single stream and date-ordered; the server computes each day independently (no last-tick carry across days), so band seams never diverge. A multi-day `.stream()` now fans out, delivering each band's chunks as they arrive and so interleaved across bands in arrival order rather than strictly date-ascending — use the buffered path, or `bulk_fetch = "off"`, when strict date order matters. A `*` option-chain `at_time` (`option_at_time_trade` / `option_at_time_quote`) shards the same way — every strike as-of a time each day is the same dense per-day compute — and reproduces the single-stream row set even at a carry-sensitive as-of time, because an option's prior-day carry resolves from the calendar day before the pull, not from a band's start date, so band seams never diverge. A concrete single-contract option `at_time` is sparse over a wide range and stays a single stream. ### Fixed - **The dedicated `*_stream()` builders now share the `.stream(handler)` chunk-delivery path.** The four dedicated streaming builders (`stock_history_trade_stream`, `stock_history_quote_stream`, `option_history_trade_stream`, `option_history_quote_stream`) previously marked their no-replay guard on every parsed chunk — including empty keepalives — and kept draining the stream after a decode failure, while the `.stream(handler)` methods on the same endpoints marked the guard only once rows actually reached the handler and stopped at the first decode failure. Both surfaces now route through the same delivery primitive, so a transient error that arrives after only empty keepalive chunks retries instead of surfacing terminal, and a decode failure ends the drain immediately — identically on both. ## [0.2.0] - 2026-07-16 ### Added - **Selectable streaming consumer wait mode (`wait_mode`) with a manual park interval, across every binding.** The event-ring consumer's idle-wait strategy is now selectable: `spin` (default, unchanged — an adaptive busy-spin then a yield ramp, never sleeps), `busyspin` (pure spin, no yield, tightest re-poll and lowest jitter), `park` (spin/yield ramp then sleep, low but fixed idle CPU), and `backoff` (spins while events flow and, after a short idle window, sleeps until events resume, then snaps back to spinning — low latency when active, low CPU when idle, the hands-free choice for a 24/7 consumer). `spin` and `busyspin` both hold ~100% of one core and differ only in jitter; only `park` and `backoff` lower idle CPU. `park` and `backoff` sleep for `park_interval_us` microseconds (default `1000` = 1 ms, validated `[50, 1_000_000]`); the OS timer honors sleeps down to ~50 us (below that kernel timer slack dominates, so 50 is the floor) and a 100 us park is a valid low-latency option that measured a few percent of a core in live premarket; the client pings roughly every 100 ms, so a park longer than the ping cadence adds delivery latency without saving further CPU. Exposed on Rust (`StreamingConfig::wait_mode` / `park_interval_us`, builder `.wait_mode()` / `.park_interval_us()`), Python (`Config.wait_mode` / `park_interval_us`), TypeScript (`waitMode` / `setWaitMode`, `parkIntervalUs` / `setParkIntervalUs`), C++ (`set_wait_mode` / `get_wait_mode`, `set_park_interval_us` / `get_park_interval_us`), and the C ABI (`thetadatadx_config_set_wait_mode` / `_get_wait_mode`, `_set_park_interval_us` / `_get_park_interval_us`), and configurable in `config.toml` under `[streaming]`. The default is byte-for-byte the previous fixed low-latency wait, so callers who do not set it are unaffected. - **Automatic bulk-fetch sharding for large history pulls, buffered and streaming.** A large history pull — a buffered `.await` on a history builder, or a chunk-streaming `.stream` / `.stream_async` call — is now sized with a cheap density probe and, when the response is big enough to benefit, split into balanced concurrent sub-requests across the account's concurrent-request budget: a multiplied bulk-download rate with no change to the call site. Buffered pulls merge the shards back into exactly the rows of the single-stream response. Row order: single-contract, stock, and index pulls keep the exact single-stream order; option-chain pulls come back in a deterministic canonical order — grouped by `(expiration, strike, right)` ascending, calls before puts, time-ascending within each contract — because the server enumerates chain contracts in an internal order no client can reproduce. Streaming pulls forward each band's chunks to the handler as they arrive — no merge, no buffering, the full fan-out throughput — so every chunk is delivered exactly once but chunks from different bands interleave in arrival order rather than the single stream's order; each band of a single-contract, stock, or index pull is internally time-ascending. Configure with `market_data.bulk_fetch` (`"auto"` default; `"off"` runs every query as a single stream in the server's own row and chunk order — use it, or the buffered path, when the single-stream order matters) and `market_data.shard_concurrency` (fan-out cap; default the account's full budget), exposed across every binding and settable in the config file under `[market_data]` (`bulk_fetch = "auto"|"off"`, `shard_concurrency`, `connect_timeout_secs`, `request_timeout_secs`, `warn_on_buffered_threshold_bytes`). Rust power users can fetch the same plan via `MarketDataClient::bulk_fetch_plan` and run the bands under their own concurrency. ### Changed - **`ping_interval_ms` default is now `100`, matching the terminal.** The client heartbeat previously defaulted to `250 ms`; the Theta Terminal pings on a fixed `100 ms` period, and with `flush_mode` removed the ping is the sole flush trigger for queued outbound control frames, so the default now matches the terminal exactly (subscribe / unsubscribe reach the server within one `100 ms` interval instead of `250 ms`). The knob and its `[100, 300_000]` range are unchanged; override it if you prefer the old cadence. - **HTTP/2 flow-control window sizes are tunable across every binding, and the market-data `window_size_kb` field is renamed to `stream_window_size_kb`.** The per-stream and per-connection HTTP/2 windows (`stream_window_size_kb`, `connection_window_size_kb`) are now exposed through the Python, TypeScript, C++, and C bindings, not just Rust. The validation clamp is raised from `[64, 1024]` to `[64, 2_097_151]` KB — the largest whole-KB value under the HTTP/2 `2^31 - 1` byte window cap — so bulk pulls are no longer throttled by a 1 MiB per-stream ceiling, and the defaults are raised to `stream_window_size_kb = 8192` / `connection_window_size_kb = 16384`. The Rust field `MarketDataConfig::window_size_kb` and the `[grpc] window_size_kb` config-file key are renamed to `stream_window_size_kb`; because the grpc config section rejects unknown keys, an existing `config.toml` using the old key now fails to load until it is renamed. This is a breaking change to the configuration surface. ### Removed - **`flush_mode` streaming write-flush knob.** The `flush_mode` setting is removed from every binding (Rust `StreamingConfig::flush_mode`, Python `Config.flush_mode`, TypeScript `Config.flushMode` / `setFlushMode`, C++ `set_flush_mode` / `get_flush_mode`, C ABI `thetadatadx_config_set_flush_mode` / `_get_flush_mode`). Outbound streaming writes now always coalesce and flush on the ping heartbeat, so a subscription burst leaves as fewer, larger packets — the terminal's own behavior — with received-data latency unaffected as before. The `"immediate"` per-frame-flush mode existed only to defeat that server-friendly coalescing and is gone. This is a breaking change to the configuration surface. - **`host_selection` / `host_shuffle_seed` streaming host-ordering knobs.** Removed from every binding (Rust `StreamingConfig::host_selection` / `host_shuffle_seed`, Python `Config.streaming_host_selection` / `streaming_host_shuffle_seed`, TypeScript `Config.streamingHostSelection` / `setStreamingHostSelection` and the shuffle-seed pair, C++ `set_streaming_host_selection` / `get_streaming_host_selection` and the shuffle-seed pair, C ABI `thetadatadx_config_*_streaming_host_selection` / `_host_shuffle_seed`), along with the `HostSelectionPolicy` enum. The client now cycles the declared host list left to right — the terminal's own behavior — and a reconnect tries the last-known-good host first. The per-client fault-domain shuffle and its seed existed only in the SDK, with no terminal counterpart. This is a breaking change to the configuration surface. - **`reconnect_jitter` trimmed to `"full"` / `"none"`.** The `"equal"` and `"decorrelated"` jitter variants are removed from every binding (the `JitterMode` enum drops them, and the C-ABI integer encoding is now `0 = Full`, `1 = None`). Full jitter — sampling uniformly from `[0, delay]` — stays the default and is the right choice for a recovering fleet; `"none"` (deterministic delays) remains for tests. The two removed variants were unused academic backoff modes with no operational benefit over full jitter. Setting `reconnect_jitter` to `"equal"` or `"decorrelated"` is now rejected. The C-ABI integer encoding renumbered — `1` now means `None` (it was `Equal`), and `2` / `3` are rejected — so a C caller that passed a numeric jitter code must be re-audited: passing `1` now selects no jitter rather than erroring. This is a breaking change to the configuration surface. - **Config-file migration.** A `config.toml` that still sets `flush_mode`, `streaming.host_selection`, `streaming.host_shuffle_seed`, or a `reconnect_jitter` of `"equal"` / `"decorrelated"` now fails to load with a typed error rather than being silently ignored — remove those keys / values when upgrading. - **`QuoteTick.midpoint` derived column.** Removed from every binding (Python `QuoteTick.midpoint`, TypeScript `QuoteTick.midpoint`, C `ThetaDataDxQuoteTick.midpoint`, and the Arrow / Polars `midpoint` column). The quote wire carries `bid` and `ask`; `midpoint` was an SDK-computed `(bid + ask) / 2` column the server never sends and the terminal never exposes — compute it from `bid` / `ask` at the call site if you need it. The `IvTick.midpoint` field is a real wire column and is unaffected. This is a breaking change to the quote tick schema. - **List endpoints now preserve wire order.** `list_*` results (roots / symbols, expirations, strikes, dates) come back in the server's row order rather than re-sorted ascending by the SDK. The terminal streams these lists in wire order untouched; the SDK's numeric-aware ascending sort had no terminal counterpart. Sort client-side if you need a specific order. This is a behavioral change to the `list_*` returns. - **`interval` is forwarded verbatim.** The `interval` argument is no longer snapped to the nearest preset: a raw-millisecond value such as `"250"` or `"60000"` is now rejected client-side rather than silently mapped to `"500ms"` / `"1m"`. The server accepts only the closed preset enum (`tick`, `10ms`, `100ms`, `500ms`, `1s`, `5s`, `10s`, `15s`, `30s`, `1m`, `5m`, `10m`, `15m`, `30m`, `1h`); the SDK now forwards the string as-is and validates against that set. Pass an explicit preset. This is a breaking change to the `interval` parameter. - **`CalendarDay.is_open` derived boolean.** Removed from every binding (Python `CalendarDay.is_open`, TypeScript `CalendarDay.isOpen`, C `ThetaDataDxCalendarDay.is_open`, and the Arrow / Polars `is_open` column). The calendar wire carries a single day-type column; `is_open` was an SDK-derived boolean (`status in {open, early_close}`) that the terminal never exposes. Read `status` instead — it carries the full `open` / `early_close` / `full_close` / `weekend` vocabulary — and treat `open` / `early_close` as trading days. This is a breaking change to the calendar tick schema. ### Fixed - **A normal `cargo build` of the Rust crate no longer requires a system `protoc`.** The crate now ships the pre-generated gRPC client code (committed at `proto/beta_endpoints.snapshot.rs` and included directly), so a default build never shells out to `protoc` — fixing build failures for users who lack a runnable `protoc`. The proto compile and its byte-for-byte drift check against `mdds.proto` are gated behind the `grpc-codegen` cargo feature (and `prost-build` moves out of the default build graph); regenerate the snapshot with `cargo run -p thetadatadx-rs --bin refresh_grpc_snapshot --features grpc-codegen`. CI still drift-checks the snapshot under that feature, so an edited `mdds.proto` with a stale snapshot fails the build. - **Python sync snapshot endpoints no longer carry a hardcoded 5-second cap.** The Python sync snapshot methods wrapped every call in a fixed 5-second timeout that fired before the caller's `timeout_ms` and the configured request timeout, making large snapshots (bulk greeks, wide option chains) unusable from the sync API. Snapshots now route through the normal request path and honor the request's own deadline. TypeScript, C++, and Rust were unaffected. ## [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`, 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 `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 `_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. [Unreleased]: https://github.com/userFRM/ThetaDataDx/compare/v0.4.0...HEAD [0.4.0]: https://github.com/userFRM/ThetaDataDx/releases/tag/v0.4.0 [0.3.0]: https://github.com/userFRM/ThetaDataDx/releases/tag/v0.3.0 [0.2.0]: https://github.com/userFRM/ThetaDataDx/releases/tag/v0.2.0 [0.1.0]: https://github.com/userFRM/ThetaDataDx/releases/tag/v0.1.0