Skip to content

Commit d1b6e42

Browse files
userFRMclaude
andcommitted
refactor(streaming): fluent contract-first API + ThetaDataClient rename
Hard break: every typed `subscribe_*` / `unsubscribe_*` / `subscribe_option_*` method on the public surface and every typed `tdx_*_subscribe_*` / `tdx_*_unsubscribe_*` C ABI entry point is deleted. The unified surface now ships only the polymorphic `subscribe(Subscription)` / `unsubscribe(Subscription)` / `subscribe_many([...])` / `unsubscribe_many([...])` paths plus the fluent `Contract::stock(...)` / `Contract::option(...)` / `SecType::Option.full_trades()` value constructors that feed them. Public client name is `ThetaDataClient` across every binding (Rust struct, Python pyclass, TypeScript napi class). The previous unified struct name is gone — no alias, no compat shim. Python also gains `AsyncThetaDataClient` as the async-only sibling that filters attribute access to `*_async` historical methods plus the streaming lifecycle helpers. Polymorphic C ABI: `tdx_unified_subscribe` / `tdx_unified_unsubscribe` / `tdx_fpss_subscribe` / `tdx_fpss_unsubscribe` take a `TdxSubscriptionRequest` payload (scope + kind + per-contract or full-stream fields). One entry point handles every variant; the generated C++ wrappers reach the same surface through the existing `tdx::Contract` / `tdx::SecType` fluent value types. The `sdk_surface.toml` codegen spec drops the typed entries; the regenerated `streaming_methods.rs` for both Python and TypeScript contains zero `subscribe_*` typed methods. CHANGELOG carries the documentation-only migration map old → new (no compat layer ships). Tests: 553 unit / integration tests pass, fmt + clippy clean, `generate_sdk_surfaces --check` returns zero diff. C++ static library builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e4b9112 commit d1b6e42

102 files changed

Lines changed: 4469 additions & 3242 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 164 additions & 91 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# ThetaDataDx
1+
# ThetaDataClient
22

33
Rust SDK for ThetaData market data — single Rust core, four language surfaces (Rust, Python, TypeScript, C++).
44

5-
[![build](https://github.com/userFRM/ThetaDataDx/actions/workflows/ci.yml/badge.svg)](https://github.com/userFRM/ThetaDataDx/actions/workflows/ci.yml)
5+
[![build](https://github.com/userFRM/ThetaDataClient/actions/workflows/ci.yml/badge.svg)](https://github.com/userFRM/ThetaDataClient/actions/workflows/ci.yml)
66
[![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
77
[![Crates.io](https://img.shields.io/crates/v/thetadatadx.svg)](https://crates.io/crates/thetadatadx)
88
[![PyPI](https://img.shields.io/pypi/v/thetadatadx)](https://pypi.org/project/thetadatadx)
@@ -48,12 +48,12 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
4848
```
4949

5050
```rust
51-
use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
51+
use thetadatadx::{ThetaDataClient, Credentials, DirectConfig};
5252

5353
#[tokio::main]
5454
async fn main() -> Result<(), thetadatadx::Error> {
5555
let creds = Credentials::from_file("creds.txt")?;
56-
let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
56+
let tdx = ThetaDataClient::connect(&creds, DirectConfig::production()).await?;
5757
let eod = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
5858
for tick in &eod {
5959
println!("{}: O={} H={} L={} C={} V={}",
@@ -86,9 +86,9 @@ pip install thetadatadx
8686
```
8787

8888
```python
89-
from thetadatadx import Credentials, Config, ThetaDataDx
89+
from thetadatadx import Credentials, Config, ThetaDataClient
9090

91-
tdx = ThetaDataDx(Credentials.from_file("creds.txt"), Config.production())
91+
tdx = ThetaDataClient(Credentials.from_file("creds.txt"), Config.production())
9292
for tick in tdx.stock_history_eod("AAPL", "20240101", "20240301"):
9393
print(f"{tick.date}: O={tick.open:.2f} H={tick.high:.2f} "
9494
f"L={tick.low:.2f} C={tick.close:.2f} V={tick.volume}")
@@ -101,9 +101,9 @@ npm install thetadatadx
101101
```
102102

103103
```typescript
104-
import { ThetaDataDx } from 'thetadatadx';
104+
import { ThetaDataClient } from 'thetadatadx';
105105

106-
const tdx = await ThetaDataDx.connectFromFile('creds.txt');
106+
const tdx = await ThetaDataClient.connectFromFile('creds.txt');
107107
for (const t of tdx.stockHistoryEOD('AAPL', '20240101', '20240301')) {
108108
console.log(`${t.date}: O=${t.open} H=${t.high} L=${t.low} C=${t.close} V=${t.volume}`);
109109
}
@@ -116,7 +116,7 @@ for (const t of tdx.stockHistoryEOD('AAPL', '20240101', '20240301')) {
116116
#include <cstdio>
117117

118118
int main() {
119-
auto tdx = thetadatadx::ThetaDataDx::connect_from_file("creds.txt");
119+
auto tdx = thetadatadx::ThetaDataClient::connect_from_file("creds.txt");
120120
for (const auto& t : tdx.stock_history_eod("AAPL", "20240101", "20240301")) {
121121
std::printf("%d: O=%.2f H=%.2f L=%.2f C=%.2f V=%lld\n",
122122
t.date, t.open, t.high, t.low, t.close, (long long)t.volume);
@@ -128,9 +128,13 @@ int main() {
128128

129129
One connection, one auth. Historical queries are available immediately; streaming connects lazily on first subscription. The client auto-reconnects and re-subscribes all active contracts on involuntary disconnect.
130130

131+
The primary streaming surface is the **fluent contract-first API**
132+
`Contract::stock("AAPL").quote()` returns a typed `Subscription` value
133+
that the polymorphic `client.subscribe(...)` accepts directly:
134+
131135
```rust
132136
use thetadatadx::fpss::{FpssData, FpssEvent};
133-
use thetadatadx::fpss::protocol::Contract;
137+
use thetadatadx::prelude::*;
134138

135139
tdx.start_streaming(|event: &FpssEvent| {
136140
match event {
@@ -140,20 +144,24 @@ tdx.start_streaming(|event: &FpssEvent| {
140144
FpssEvent::Data(FpssData::Trade { contract, price, size, .. }) => {
141145
println!("Trade: {} @ {price} x {size}", contract.symbol);
142146
}
143-
FpssEvent::Data(FpssData::Ohlcvc { contract, open, high, low, close, volume, .. }) => {
144-
println!(
145-
"OHLCVC: {} O={open} H={high} L={low} C={close} V={volume}",
146-
contract.symbol,
147-
);
148-
}
149147
_ => {}
150148
}
151149
})?;
152150

153-
tdx.subscribe_quotes(&Contract::stock("AAPL"))?;
154-
tdx.subscribe_trades(&Contract::stock("AAPL"))?;
151+
let stock = Contract::stock("AAPL");
152+
let option = Contract::option("SPY", "20260620", "550", "C")?;
153+
154+
tdx.subscribe(stock.quote())?;
155+
tdx.subscribe(option.trade())?;
156+
tdx.subscribe(SecType::Option.full_open_interest())?;
157+
158+
// Bulk install:
159+
tdx.subscribe_many(vec![stock.quote(), option.quote()])?;
155160
```
156161

162+
All prices (`bid`, `ask`, `price`, `open`, `high`, `low`, `close`) are `f64`, decoded during parsing.
163+
164+
157165
All prices (`bid`, `ask`, `price`, `open`, `high`, `low`, `close`) are `f64`, decoded during parsing.
158166

159167
## API coverage

crates/tdbe/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ println!("IV: {:.4}, Delta: {:.4}", result.iv, result.delta);
4949

5050
`thetadatadx` depends on `tdbe` for all data types and codecs, then adds
5151
networking (gRPC historical via MDDS, real-time streaming via FPSS), authentication,
52-
and the unified `ThetaDataDx` client. If you only need types and offline Greeks,
52+
and the unified `ThetaDataClient` client. If you only need types and offline Greeks,
5353
depend on `tdbe` alone.
5454

5555
## License

crates/thetadatadx/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ serde_json = "1.0.137"
117117
zeroize = { version = "1.8.2", features = ["derive"] }
118118

119119
# Lock-free atomic swap of an `Arc<StreamingSlot>` for the streaming
120-
# state machine on `ThetaDataDx`. Carries the `Arc<FpssClient>` so the
120+
# state machine on `ThetaDataClient`. Carries the `Arc<FpssClient>` so the
121121
# hot read paths (`is_streaming`, `connection_status`,
122122
# `dropped_event_count`, `panic_count`, `with_streaming`) all collapse
123123
# to a single `state.load()`.

crates/thetadatadx/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
Core Rust crate — direct wire-protocol access to all three of ThetaData's public surfaces: MDDS (request/response history over gRPC), FPSS (real-time streaming over TCP), and FLATFILES (whole-universe daily blobs over the legacy MDDS port).
44

5-
This is the engine that powers all ThetaDataDx SDKs (Python, TypeScript / Node.js, C++, CLI, MCP, REST server).
5+
This is the engine that powers all ThetaDataClient SDKs (Python, TypeScript / Node.js, C++, CLI, MCP, REST server).
66

77
## Entry Point
88

99
```rust
10-
use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
10+
use thetadatadx::{ThetaDataClient, Credentials, DirectConfig};
1111
use thetadatadx::fpss::protocol::Contract;
1212

1313
let creds = Credentials::from_file("creds.txt")?;
14-
let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
14+
let tdx = ThetaDataClient::connect(&creds, DirectConfig::production()).await?;
1515

1616
// Historical - typed endpoints available immediately
1717
let eod = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
1818

1919
// Streaming - connects lazily on first call
2020
tdx.start_streaming(|event| { /* ... */ })?;
21-
tdx.subscribe_quotes(&Contract::stock("AAPL"))?;
21+
tdx.subscribe(Contract::stock("AAPL").quote())?;
2222

2323
// When done
2424
tdx.stop_streaming();
@@ -35,14 +35,14 @@ let rows = tdx
3535
.await?;
3636
```
3737

38-
`ThetaDataDx::connect()` authenticates once. Historical data (MDDS gRPC) is available immediately via `Deref` to the internal `MddsClient`. Streaming (FPSS TCP) connects lazily when you call `start_streaming()`. Flat-file requests (FLATFILES) open a per-call TLS connection to the legacy MDDS port and are independent of the MDDS gRPC and FPSS sessions — see [vendor docs](https://http-docs.thetadata.us/operations/get-v2-flat-file-getting-started.html) for the full flat-file matrix.
38+
`ThetaDataClient::connect()` authenticates once. Historical data (MDDS gRPC) is available immediately via `Deref` to the internal `MddsClient`. Streaming (FPSS TCP) connects lazily when you call `start_streaming()`. Flat-file requests (FLATFILES) open a per-call TLS connection to the legacy MDDS port and are independent of the MDDS gRPC and FPSS sessions — see [vendor docs](https://http-docs.thetadata.us/operations/get-v2-flat-file-getting-started.html) for the full flat-file matrix.
3939

4040
## Crate Layout
4141

4242
```
4343
src/
44-
lib.rs - public re-exports (ThetaDataDx, Credentials, DirectConfig, Error)
45-
unified.rs - ThetaDataDx: single entry point, lazy streaming
44+
lib.rs - public re-exports (ThetaDataClient, Credentials, DirectConfig, Error)
45+
unified.rs - ThetaDataClient: single entry point, lazy streaming
4646
mdds/ - MddsClient module (client, stream, validate, normalize, endpoints)
4747
auth/ - Nexus API authentication, credential parsing
4848
fpss/ - FPSS streaming client (sync, LMAX Disruptor ring buffer)

crates/thetadatadx/build_support/endpoints/render/python.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Python SDK `#[pymethods]` emitter.
22
//!
3-
//! Renders `sdks/python/src/historical_methods.rs` — the `ThetaDataDx` impl
3+
//! Renders `sdks/python/src/historical_methods.rs` — the `ThetaDataClient` impl
44
//! block that PyO3 compiles into the Python extension.
55
//!
66
//! Every method takes a `timeout_ms: Optional[int]` kwarg. When set the
77
//! call is given a deadline; on expiry a `RuntimeError` carrying the
88
//! `Error::Timeout` message is raised and the underlying gRPC stream is
9-
//! cancelled. The `ThetaDataDx` handle remains usable for subsequent calls.
9+
//! cancelled. The `ThetaDataClient` handle remains usable for subsequent calls.
1010
//!
1111
//! # Three variants per endpoint (sync / async / fluent builder)
1212
//!
@@ -82,7 +82,7 @@ pub(super) fn render_python_decode_bench(endpoints: &[GeneratedEndpoint]) -> Str
8282

8383
// The pyfunction itself.
8484
out.push_str("/// Decode recorded gRPC response bytes into the same typed pyclass list\n");
85-
out.push_str("/// the matching `ThetaDataDx` endpoint would have returned.\n");
85+
out.push_str("/// the matching `ThetaDataClient` endpoint would have returned.\n");
8686
out.push_str("///\n");
8787
out.push_str("/// `endpoint` must match an endpoint name from `endpoint_surface.toml` that\n");
8888
out.push_str(
@@ -163,7 +163,7 @@ pub(super) fn render_python_historical_methods(endpoints: &[GeneratedEndpoint])
163163
"// @generated DO NOT EDIT — regenerated by build.rs from endpoint_surface.toml\n\n",
164164
);
165165

166-
// Per-endpoint fluent builder pyclasses. Emitted BEFORE the `ThetaDataDx`
166+
// Per-endpoint fluent builder pyclasses. Emitted BEFORE the `ThetaDataClient`
167167
// impl block so `add_class::<HistoricalBuilder>()` at module-init time sees
168168
// the types. One struct per endpoint; each has chained setters for the
169169
// optional params, required params captured at construction time, and
@@ -199,7 +199,7 @@ pub(super) fn render_python_historical_methods(endpoints: &[GeneratedEndpoint])
199199
out.push_str("}\n\n");
200200

201201
out.push_str("#[pymethods]\n");
202-
out.push_str("impl ThetaDataDx {\n");
202+
out.push_str("impl ThetaDataClient {\n");
203203
for endpoint in endpoints
204204
.iter()
205205
.filter(|endpoint| !is_streaming_endpoint(endpoint))
@@ -486,7 +486,7 @@ fn render_python_endpoint_async(endpoint: &GeneratedEndpoint) -> String {
486486
out.push_str(" timeout_ms: Option<u64>,\n");
487487
out.push_str(" ) -> PyResult<Bound<'py, PyAny>> {\n");
488488

489-
// Clone the Arc<thetadatadx::ThetaDataDx> handle into the closure — the
489+
// Clone the Arc<thetadatadx::ThetaDataClient> handle into the closure — the
490490
// inner client is not Clone but the Arc is, and the builder pyclass
491491
// pattern elsewhere in this file relies on the same contract.
492492
out.push_str(" let tdx = self.tdx.clone();\n");
@@ -689,8 +689,8 @@ fn async_setter_arg_expr(param: &GeneratedParam) -> &'static str {
689689
//
690690
// The builder pattern is a Python-side ergonomics layer: chain setters,
691691
// terminate with `.arrow()` / `.list()` / `.polars()` / `.pandas()`. The
692-
// builder struct clones an `Arc<thetadatadx::ThetaDataDx>` from the owning
693-
// `ThetaDataDx` pyclass (see lib.rs where the field is wrapped in Arc for
692+
// builder struct clones an `Arc<thetadatadx::ThetaDataClient>` from the owning
693+
// `ThetaDataClient` pyclass (see lib.rs where the field is wrapped in Arc for
694694
// exactly this reason), so terminal calls can drive the existing endpoint
695695
// methods on the underlying client without re-authenticating or copying
696696
// configuration. State captured at builder construction time: required
@@ -761,9 +761,9 @@ fn render_python_endpoint_builder(endpoint: &GeneratedEndpoint) -> String {
761761
)
762762
.unwrap();
763763
writeln!(out, "pub struct {struct_name} {{").unwrap();
764-
// Same Arc the parent `ThetaDataDx` pyclass stores — cheap to clone,
764+
// Same Arc the parent `ThetaDataClient` pyclass stores — cheap to clone,
765765
// so the async terminal can move it into the future.
766-
out.push_str(" tdx: std::sync::Arc<thetadatadx::ThetaDataDx>,\n");
766+
out.push_str(" tdx: std::sync::Arc<thetadatadx::ThetaDataClient>,\n");
767767
for param in &method_params {
768768
if param.param_type == "Symbols" {
769769
out.push_str(" symbols: Vec<String>,\n");
@@ -940,7 +940,7 @@ fn render_python_endpoint_builder(endpoint: &GeneratedEndpoint) -> String {
940940
out
941941
}
942942

943-
/// Builder constructor emitted on `ThetaDataDx` — `tdx.name_builder(...)` is
943+
/// Builder constructor emitted on `ThetaDataClient` — `tdx.name_builder(...)` is
944944
/// the Python entry point that yields a `NameBuilder`. Required params are
945945
/// passed positionally (same order as the typed endpoint); optionals are
946946
/// chained after construction.

crates/thetadatadx/build_support/endpoints/render/templates/validate_python/preamble.py.tmpl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ baseline; bulk cells (`all_strikes_one_exp`, `bulk_chain` on option
1717
history / at-time endpoints) use 180 seconds because a full-chain
1818
payload legitimately takes longer than a minute. On expiry the in-flight
1919
gRPC stream is cancelled and a RuntimeError carrying "Request deadline
20-
exceeded" is raised. The `ThetaDataDx` handle stays usable for
20+
exceeded" is raised. The `ThetaDataClient` handle stays usable for
2121
subsequent cells.
2222
"""
2323
import json
2424
import pathlib
2525
import sys
2626
import time
2727

28-
from thetadatadx import Credentials, Config, ThetaDataDx
28+
from thetadatadx import Credentials, Config, ThetaDataClient
2929

3030
PER_CELL_TIMEOUT_MS = 60_000
3131
SLOW_MODE_TIMEOUT_MS = 180_000
3232
ARTIFACT_PATH = pathlib.Path(__file__).resolve().parents[1] / "artifacts" / "validator_python.json"
3333

34-
client = ThetaDataDx(Credentials.from_file(sys.argv[1]), Config.production())
34+
client = ThetaDataClient(Credentials.from_file(sys.argv[1]), Config.production())
3535

3636
# (endpoint, mode_name, declared_min_tier, rationale, callable)
3737
# `declared_min_tier` is informational only (printed on tier-permission

crates/thetadatadx/build_support/endpoints/render/typescript.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
//! TypeScript SDK napi-rs emitter.
22
//!
3-
//! Renders `sdks/typescript/src/historical_methods.rs` — the `ThetaDataDx`
3+
//! Renders `sdks/typescript/src/historical_methods.rs` — the `ThetaDataClient`
44
//! impl block that napi-rs compiles into the Node.js native addon.
55
//!
66
//! Every method takes an optional `timeout_ms` parameter. When set, the call
77
//! is given a deadline; on expiry a JS error is thrown and the underlying gRPC
8-
//! stream is cancelled. The `ThetaDataDx` handle remains usable.
8+
//! stream is cancelled. The `ThetaDataClient` handle remains usable.
99
1010
use std::fmt::Write as _;
1111

@@ -23,7 +23,7 @@ pub(super) fn render_typescript_historical_methods(endpoints: &[GeneratedEndpoin
2323
"// @generated DO NOT EDIT — regenerated by build.rs from endpoint_surface.toml\n\n",
2424
);
2525
out.push_str("#[napi]\n");
26-
out.push_str("impl ThetaDataDx {\n");
26+
out.push_str("impl ThetaDataClient {\n");
2727
for endpoint in endpoints
2828
.iter()
2929
.filter(|endpoint| !is_streaming_endpoint(endpoint))

crates/thetadatadx/build_support/sdk_surface/python.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub(super) fn render_python_streaming_methods(methods: &[&MethodSpec]) -> String
99
let mut out = String::new();
1010
out.push_str(generated_header());
1111
out.push_str("#[pymethods]\n");
12-
out.push_str("impl ThetaDataDx {\n");
12+
out.push_str("impl ThetaDataClient {\n");
1313
for method in methods {
1414
out.push_str(&python_streaming_method(method));
1515
out.push('\n');
@@ -254,7 +254,7 @@ fn python_streaming_method(method: &MethodSpec) -> String {
254254
Requires a prior `start_streaming(callback)`; raises\n\
255255
`RuntimeError` if no callback is registered. All\n\
256256
active subscriptions are restored on the new\n\
257-
connection — see `thetadatadx::ThetaDataDx::reconnect_streaming`\n\
257+
connection — see `thetadatadx::ThetaDataClient::reconnect_streaming`\n\
258258
for partial-failure semantics.\n\
259259
\n\
260260
# Callback lifetime across `stop_streaming`\n\

crates/thetadatadx/build_support/sdk_surface/templates/mcp/try_execute_preamble.rs.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
async fn try_execute_generated_utility(
2-
client: Option<&ThetaDataDx>,
2+
client: Option<&ThetaDataClient>,
33
name: &str,
44
args: &Value,
55
start_time: std::time::Instant,

0 commit comments

Comments
 (0)