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.
0.4.0 - 2026-08-08
Removed
MarketDataClient::bulk_fetch_plan, and the publicShardPlanandShardQuerytypes. 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_mbconfig-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 setsmax_message_size_mbnow 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 (
authbeyondCredentials,backoffbeyondJitterMode,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 theauthorbackoffmodule 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). TheShardBand::Dateband (surfaced only throughError::PartialShardFetch) gains arightfield; 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 JSDate. ADateis 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 queried20231231). These parameters now accept the wire-format string exclusively. This is a breaking change to the TypeScript surface; format aDateyourself before passing it.The TypeScript TCP-keepalive setters name their argument
secs, notms.setStreamingKeepaliveIdleSecsandsetStreamingKeepaliveIntervalSecsstore seconds, but the parameter was labeledms, so an editor hint invited a 1000× unit mistake. The label is nowsecs, matching the Python binding; positional callers are unaffected.StreamingClient.streaming()returns aStandaloneStreamingSession. The standalone streaming client's session was typed asStreamingSession(aClientandStreamView), sosession.marketData,session.flatFiles, andsession.close()type-checked but wereundefinedat runtime. The standalone session is now its own type extending onlyStreamingClient, so the type surface matches what the object exposes.
Fixed
Rejected stream subscriptions now log at
warninstead ofdebug. A subscribe the server rejects (MaxStreamsReached,InvalidPerms, or a generic error) is dropped; recording that only atdebugleft a capped or unentitled stream looking live under a default log subscriber. It now logs atwarnwith 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 = "*"orexpiration = "*", 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, andat_timechains 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'sblock_on(synchronous streaming), where a reentrant synchronous call would deadlock on the held request permit or abort the runtime with a nestedblock_onbefore reaching the core guard. The binding now detects the re-entry on that thread and raisesHandlerReentrancyimmediately; issue the request outside the handler (for example from a background thread).Per-call
timeout_ms = 0disables the deadline on every endpoint family. On the Python and TypeScript list endpoints (option_list_expirations,stock_list_symbols, and the like) a zerotimeout_ms/timeoutMsraced the call against a zero-duration timer and failed immediately, while the history and snapshot endpoints read zero as "no deadline" (matchingwith_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, sosetMaxConcurrentRequests(-1)wrapped to4294967295and drove a multi-billion-entry pool allocation,1.5truncated to1, and2**32wrapped to0. The setters now take the value as a number and reject a non-finite, negative, fractional, or over-u32input withInvalidParameterError; the port setters keep their0..=65535range check on top.A rejected BigInt config value names its actual cause. A negative or over-
u64BigInt passed to a*Msconfig 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-u64magnitude). The error stays a plainError, matching the built-inOverflowErrorthe 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_typeand 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 mapssec_type(STOCK/INDEX, a null or empty value defaulting to stock for compatibility, any other value rejected asTHETADATADX_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_streamingor assigned toreconnect_callbackwas accepted and only failed later on a delivery thread as an unraisableTypeError. Both now raiseInvalidParameterErrorsynchronously.Contract.option(right=...)accepts aRightenum. The Python contract builder tookrightas a plainstr, so passing the SDK's ownRight.CALLraised aTypeError; it now accepts aRight(through its.value) as well as a string.Client.from_dotenvkeeps the file's environment and host settings when a single channel selector is overridden. Passing onlymarket_data_type=(orstreaming_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.AsyncClientsurfaces the StreamView health getters and the flat-file async terminals. The async proxy's allowlist omittedis_authenticated,millis_since_last_event,last_event_received_at_unix_nanos,last_connected_addr, andbatches, so those valid operations raisedAttributeError, and every*_asyncname routed tomarket_data, soflatfile_to_path_asynccould not resolve. The health getters resolve like their sibling diagnostics, and a flat-file*_asyncname falls back to theflat_filesnamespace.Stream::batchesrejects a negative linger. A negativelingeron the C++ Arrow batch reader silently became an immediate flush; it now throwsInvalidParameterError, matching the Python and TypeScript bindings.A malformed endpoint argument is reported as
THETADATADX_ERR_INVALID_PARAMETERon the C ABI. A rejected request-option argument (for example a boolean that is neither0nor1) set the untypedTHETADATADX_ERR_OTHER, so the C++ wrapper threw the generic base exception instead ofInvalidParameterError; 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.tstype — 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++ reconnectuser_datalifetime and the tick-chunk callback's possible worker-thread affinity are stated, the Pythonfrom_envhelpers name theInvalidParameterErrora missingTHETADATA_API_KEYactually raises rather thanConfigError, and the streaming shutdown docs no longer claim a repeatedvoidshutdown 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_requestsconfig field overrides it with no client-side cap. The SDK previously treated the tier's2^tierconcurrency (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: leavingmax_concurrent_requestsunset 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 = 32in Python,setMaxConcurrentRequestsin TypeScript,set_max_concurrent_requestsin C++ and the C ABI,[market_data] max_concurrent_requestsin the TOML config file), and bulk-fetch sharding fans out across the full resolved pool — a boosted account now shards past the old2^tierceiling. The server remains the enforcement point: requests past the account's real allowance are rejected asResourceExhaustedand 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 withright = "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. Underbulk_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, orbulk_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_planis nowfn(&self, endpoint, query) -> Option<ShardPlan>(wasasync fn -> Result<Option<ShardPlan>, 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.awaitand theResulthandling. This is a breaking change to the Rust API.A failed band no longer takes down a large sharded pull. A buffered
.awaitband 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_asyncpull, 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 newError::PartialShardFetchnaming 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 sizewith_deadline/timeout_msto the whole pull. Mapped toStreamError/THETADATADX_ERR_STREAMon 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::HandlerReentrancyimmediately, 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
debugwhy 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_timehistory now shards along the date axis.stock_at_time_trade,stock_at_time_quote, andindex_at_time_pricereturn 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, orbulk_fetch = "off", when strict date order matters. A*option-chainat_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 optionat_timeis 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), andbackoff(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).spinandbusyspinboth hold ~100% of one core and differ only in jitter; onlyparkandbackofflower idle CPU.parkandbackoffsleep forpark_interval_usmicroseconds (default1000= 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 inconfig.tomlunder[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
.awaiton a history builder, or a chunk-streaming.stream/.stream_asynccall — 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 withmarket_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) andmarket_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 viaMarketDataClient::bulk_fetch_planand run the bands under their own concurrency.
Changed
ping_interval_msdefault is now100, matching the terminal. The client heartbeat previously defaulted to250 ms; the Theta Terminal pings on a fixed100 msperiod, and withflush_moderemoved 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 one100 msinterval instead of250 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_kbfield is renamed tostream_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/22^31 - 1byte window cap — so bulk pulls are no longer throttled by a 1 MiB per-stream ceiling, and the defaults are raised tostream_window_size_kb = 8192/connection_window_size_kb = 16384. The Rust fieldMarketDataConfig::window_size_kband the[grpc] window_size_kbconfig-file key are renamed tostream_window_size_kb; because the grpc config section rejects unknown keys, an existingconfig.tomlusing the old key now fails to load until it is renamed. This is a breaking change to the configuration surface.
Removed
flush_modestreaming write-flush knob. Theflush_modesetting is removed from every binding (RustStreamingConfig::flush_mode, PythonConfig.flush_mode, TypeScriptConfig.flushMode/setFlushMode, C++set_flush_mode/get_flush_mode, C ABIthetadatadx_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_seedstreaming host-ordering knobs. Removed from every binding (RustStreamingConfig::host_selection/host_shuffle_seed, PythonConfig.streaming_host_selection/streaming_host_shuffle_seed, TypeScriptConfig.streamingHostSelection/setStreamingHostSelectionand the shuffle-seed pair, C++set_streaming_host_selection/get_streaming_host_selectionand the shuffle-seed pair, C ABIthetadatadx_config_*_streaming_host_selection/_host_shuffle_seed), along with theHostSelectionPolicyenum. 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_jittertrimmed to"full"/"none". The"equal"and"decorrelated"jitter variants are removed from every binding (theJitterModeenum drops them, and the C-ABI integer encoding is now0 = 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. Settingreconnect_jitterto"equal"or"decorrelated"is now rejected. The C-ABI integer encoding renumbered —1now meansNone(it wasEqual), and2/3are rejected — so a C caller that passed a numeric jitter code must be re-audited: passing1now selects no jitter rather than erroring. This is a breaking change to the configuration surface.Config-file migration. A
config.tomlthat still setsflush_mode,streaming.host_selection,streaming.host_shuffle_seed, or areconnect_jitterof"equal"/"decorrelated"now fails to load with a typed error rather than being silently ignored — remove those keys / values when upgrading.QuoteTick.midpointderived column. Removed from every binding (PythonQuoteTick.midpoint, TypeScriptQuoteTick.midpoint, CThetaDataDxQuoteTick.midpoint, and the Arrow / Polarsmidpointcolumn). The quote wire carriesbidandask;midpointwas an SDK-computed(bid + ask) / 2column the server never sends and the terminal never exposes — compute it frombid/askat the call site if you need it. TheIvTick.midpointfield 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 thelist_*returns.intervalis forwarded verbatim. Theintervalargument 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 theintervalparameter.CalendarDay.is_openderived boolean. Removed from every binding (PythonCalendarDay.is_open, TypeScriptCalendarDay.isOpen, CThetaDataDxCalendarDay.is_open, and the Arrow / Polarsis_opencolumn). The calendar wire carries a single day-type column;is_openwas an SDK-derived boolean (status in {open, early_close}) that the terminal never exposes. Readstatusinstead — it carries the fullopen/early_close/full_close/weekendvocabulary — and treatopen/early_closeas trading days. This is a breaking change to the calendar tick schema.
Fixed
A normal
cargo buildof the Rust crate no longer requires a systemprotoc. The crate now ships the pre-generated gRPC client code (committed atproto/beta_endpoints.snapshot.rsand included directly), so a default build never shells out toprotoc— fixing build failures for users who lack a runnableprotoc. The proto compile and its byte-for-byte drift check againstmdds.protoare gated behind thegrpc-codegencargo feature (andprost-buildmoves out of the default build graph); regenerate the snapshot withcargo run -p thetadatadx-rs --bin refresh_grpc_snapshot --features grpc-codegen. CI still drift-checks the snapshot under that feature, so an editedmdds.protowith 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_msand 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 — theflat_files()namespace view plus theflatfile_request/flatfile_request_decodedentries and the five per-dataset convenience methods — matching the unifiedClientmethod-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++
FlatFilesview 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 plainconst(safe on a temporary), matching themarket_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 companionAsyncClient) exposes point-in-time history undermarket_data, real-time streaming understream, and whole-universe flat files on the client directly (client.market_data.stock_history_eod(...),client.stream.subscribe(...)). Single-purposeMarketDataClientandStreamingClientare available on every binding. API-key authentication is supplied inline, read fromTHETADATA_API_KEY, or loaded from a.envfile, 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/PUTrights, 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 unauthenticatedGET /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 acceptformat=html; flat-file downloads acceptcsv(default),json,ndjson/jsonl, andhtmlon the terminal'sGET /v3/{sec_type}/flat_file/{req_type}route. On the WebSocket, an optionstrikedefaults to the terminal's 1/10-cent integer (--strike-format dollarsswitches to a dollar value), and every message header carries the streamingstatus. - MCP server on npm. The
thetadatadx-mcp-serverserver runs withnpx -y thetadatadx-mcp-server(no Rust toolchain required;cargo installstays 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, optionopen_interest, the intraday greeks and trade-greeks families, and indexprice) takes an optionaldateplus optionalstart_date/end_date: supplydatefor a single day, or a range on the base route, matching the terminal. - Unambiguous field units and semantics.
strikemeans dollars on every typed surface (float/double), with the exact wire integer reachable under the unit-namedstrike_thousandths(a$550.00strike is550000). The optionrightis the logical character ('C'/'P'; auint32_tUnicode scalar in C).EodTicktime columns arecreated_ms_of_dayandlast_trade_ms_of_day.CalendarDay.is_openis a boolean andstatuscarries theopen/early_close/full_close/weekendvocabulary. Absent contract identity isNone/undefined(Arrow null) in Python and TypeScript; the C-layout rows carry documented fills withhas_contract_id().Contract.option(...)takes the leg as one namedOptionLeg { expiration, strike, right }. - Columnar and Arrow across every binding. A pull-based Arrow
RecordBatchreader,batches(), delivers decoded streaming events as Arrow record batches on a schedule you control (Rustfutures::Stream+.blocking(), Python sync/async iterable over the Arrow C-Data interface, TypeScriptAsyncIterable<RecordBatch>, C++arrow::RecordBatchReader), tunable bybatch_sizeand alingerflush withBlockorDropOldestbackpressure and adropped()/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>WithColumnsand the C / C++ column-presence out-param; multi-symbol snapshots attribute each row to its ownsymbol. 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::MarketValuewithmarket_bid/market_ask/ integer-midpointmarket_price, subscribed viaContract::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 })). PythonAsyncClientgains awaitable constructors and*_asyncflat-file twins; C++ gains an<endpoint>_async(...)companion returningstd::futurefor 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
ConfigErrorleaf for environmental faults,InvalidParameterErrorfor rejected configuration, sequence, and flat-file inputs (and, in Python, a subclass of the built-inValueError), andStreamErrorfor streaming faults. Python addsNotFoundError/DeadlineExceededError/UnavailableError(withNoDataFoundError/TimeoutErroraliases). Rate-limit errors expose the decoded server back-off (retry_after/retryAfter/retry_after(); the C-ABIthetadatadx_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 optionalconsumer_cpuknob pins the streaming consumer thread for low-jitter delivery; and epoch-instant accessors (*_timestamp_ms, plusthetadatadx::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_threadsknob. Config sections aremarket_dataandstreaming, each channel selected independently (MarketDataEnvironmentPROD/STAGE,StreamingEnvironmentPROD/DEV) via per-binding setters and theTHETADATA_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.
FlatFileFormatcoversCsv,Jsonl,Json(a single JSON array), andHtml(an HTML table) on the SDK surface, restricted to the datasets actually served — optiontrade_quote/open_interest/eodand stocktrade_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. Afrom_fileclient-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.strikeisdoubledollars with a trailingint32_t strike_thousandths; per-tickrightfields areuint32_tUnicode scalars; andThetaDataDxCalendarDay.is_openis a C99bool. The workspace builds and publishes a singlethetadatadxartifact — every row carries its price as decodedf64dollars, and time-and-calendar support is a private internal module.
Security
- Every client TLS configuration is built with an explicit
ringcrypto provider and explicit protocol versions rather than a process-global default, soringis 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
Debugoutput.