Skip to content

Commit 2d79cbf

Browse files
authored
v3.0.0: unified ThetaDataDx client -- one connect, lazy streaming
Breaking: - ThetaDataDx replaces DirectClient + FpssClient as the public API - Connect once, auth once. Historical immediately, streaming on demand. - Python: DirectClient and FpssClient classes removed. Use ThetaDataDx. Added: - ThetaDataDx::connect(creds, config) -- single entry point - start_streaming / start_streaming_no_ohlcvc -- lazy FPSS - stop_streaming -- clean shutdown, historical stays alive - Explicit Drop impl stops streaming automatically - FFI: tdx_unified_connect, tdx_unified_historical (repr(transparent) cast) - C++ SDK: 9 new typed structs, all 61 methods return vector<T> - Go SDK: 9 new typed structs, all 63 methods return []T - Python: _df DataFrame methods on ThetaDataDx - Server: stop_streaming on graceful shutdown Fixed: - All audit findings (critical/high/medium/low) from dual Codex+Gemini review - No raw JSON in any SDK public API surface - All docs, notebooks, READMEs match actual API signatures 160 tests, zero warnings.
1 parent ce842e8 commit 2d79cbf

96 files changed

Lines changed: 4785 additions & 3480 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.

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ A clear and concise description of the bug.
2121

2222
## Endpoint / Method Affected
2323

24-
Which API endpoint or client method is involved? (e.g., `DirectClient::get_quotes`, `FpssClient::subscribe`)
24+
Which API endpoint or client method is involved? (e.g., `ThetaDataDx::stock_history_eod`, `ThetaDataDx::subscribe_quotes`)
2525

2626
## Steps to Reproduce
2727

CHANGELOG.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.0.0] - 2026-03-27
9+
10+
### Breaking Changes
11+
12+
- **Unified `ThetaDataDx` client** — single entry point replacing `DirectClient` + `FpssClient`.
13+
Connect once, auth once. Historical available immediately, streaming connects lazily.
14+
- **`DirectClient` removed from crate root re-exports** — still accessible as `thetadatadx::direct::DirectClient` but all methods available via `ThetaDataDx` (Deref)
15+
- **`FpssClient` removed from crate root re-exports** — use `tdx.start_streaming(handler)` instead
16+
- **Python SDK**: `DirectClient` and `FpssClient` classes removed. Use `ThetaDataDx` only.
17+
18+
### Added
19+
20+
- `ThetaDataDx::connect(creds, config)` — one auth, gRPC channel ready, no FPSS yet
21+
- `tdx.start_streaming(handler)` — lazy FPSS connection on demand
22+
- `tdx.start_streaming_no_ohlcvc(handler)` — same, without derived OHLCVC
23+
- `tdx.stop_streaming()` — clean shutdown of streaming, historical stays alive
24+
- `tdx.is_streaming()` — check if FPSS is active
25+
- All 61 historical methods via `Deref<Target = DirectClient>`
26+
- All streaming methods (subscribe/unsubscribe) directly on `ThetaDataDx`
27+
- FFI: `tdx_unified_connect()`, `tdx_unified_start_streaming()`, `tdx_unified_stop_streaming()`
28+
- Server: graceful `stop_streaming()` on shutdown
29+
30+
### Fixed
31+
32+
- Server shutdown now calls `stop_streaming()` before notifying waiters
33+
- Python SDK: removed duplicate method definitions (DirectClient + ThetaDataDx had same methods)
34+
835
## [2.0.0] - 2026-03-27
936

1037
### New Products
@@ -312,7 +339,9 @@ See [TODO.md](TODO.md) for the production readiness checklist and performance ro
312339
- FIT decoder uses i64 accumulator with i32 saturation (no silent overflow)
313340
- Price type range enforced with `assert!` in release builds
314341

315-
[Unreleased]: https://github.com/userFRM/ThetaDataDx/compare/v1.2.0...HEAD
342+
[Unreleased]: https://github.com/userFRM/ThetaDataDx/compare/v3.0.0...HEAD
343+
[3.0.0]: https://github.com/userFRM/ThetaDataDx/compare/v2.0.0...v3.0.0
344+
[2.0.0]: https://github.com/userFRM/ThetaDataDx/compare/v1.2.2...v2.0.0
316345
[1.2.2]: https://github.com/userFRM/ThetaDataDx/compare/v1.2.1...v1.2.2
317346
[1.2.1]: https://github.com/userFRM/ThetaDataDx/compare/v1.2.0...v1.2.1
318347
[1.2.0]: https://github.com/userFRM/ThetaDataDx/compare/v1.1.1...v1.2.0

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Adding a new endpoint is a single macro invocation -- no hand-coded boilerplate.
8282
- Add the corresponding `extern "C"` function in `ffi/src/lib.rs`
8383
- The FFI crate also uses macros for endpoint generation -- follow the existing pattern
8484
- For FPSS-related functions, see the 7 existing `thetadatadx_fpss_*` functions as examples
85-
- Update the C header, Go SDK (`FpssClient`), and C++ SDK (`FpssClient`) wrappers accordingly
85+
- Update the C header, Go SDK, and C++ SDK wrappers accordingly
8686

8787
4. **Expose in the Python SDK**
8888
- Add the PyO3 binding in `sdks/python/src/`

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ No-JVM ThetaData Terminal — native Rust SDK for direct market data access.
3434

3535
```toml
3636
[dependencies]
37-
thetadatadx = "2.0"
37+
thetadatadx = "3.0"
3838
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
3939
```
4040

@@ -53,23 +53,23 @@ pip install thetadatadx[pandas]
5353
> Create a `creds.txt` file with your ThetaData email on line 1 and password on line 2. This is the same format the Java terminal uses.
5454
5555
```rust
56-
use thetadatadx::{DirectClient, Credentials, DirectConfig};
56+
use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
5757

5858
#[tokio::main]
5959
async fn main() -> Result<(), thetadatadx::Error> {
6060
let creds = Credentials::from_file("creds.txt")?;
61-
let client = DirectClient::connect(&creds, DirectConfig::production()).await?;
61+
let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
6262

6363
// Fetch end-of-day stock data
64-
let eod = client.stock_history_eod("AAPL", "20240101", "20240301").await?;
64+
let eod = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
6565
for tick in &eod {
6666
println!("{}: O={} H={} L={} C={} V={}",
6767
tick.date, tick.open_price(), tick.high_price(),
6868
tick.low_price(), tick.close_price(), tick.volume);
6969
}
7070

7171
// List option expirations
72-
let exps = client.option_list_expirations("SPY").await?;
72+
let exps = tdx.option_list_expirations("SPY").await?;
7373
println!("SPY expirations: {:?}", &exps[..5.min(exps.len())]);
7474

7575
Ok(())
@@ -82,14 +82,17 @@ async fn main() -> Result<(), thetadatadx::Error> {
8282
> FPSS streaming connects to ThetaData's dedicated streaming servers via TLS/TCP. The client automatically sends heartbeat pings every 100ms as required by the protocol.
8383
8484
```rust
85-
use thetadatadx::auth::Credentials;
86-
use thetadatadx::fpss::{FpssClient, FpssData, FpssControl, FpssEvent};
85+
use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
86+
use thetadatadx::fpss::{FpssData, FpssControl, FpssEvent};
8787
use thetadatadx::fpss::protocol::Contract;
8888

8989
#[tokio::main]
9090
async fn main() -> Result<(), thetadatadx::Error> {
9191
let creds = Credentials::from_file("creds.txt")?;
92-
let client = FpssClient::connect(&creds, 1024, |event: &FpssEvent| {
92+
let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
93+
94+
// Start streaming with a callback
95+
tdx.start_streaming(|event: &FpssEvent| {
9396
match event {
9497
FpssEvent::Data(FpssData::Quote { contract_id, bid, ask, .. }) => {
9598
println!("Quote: contract={contract_id} bid={bid} ask={ask}");
@@ -104,11 +107,12 @@ async fn main() -> Result<(), thetadatadx::Error> {
104107
}
105108
})?;
106109

107-
let req_id = client.subscribe_quotes(&Contract::stock("AAPL"))?;
108-
println!("Subscribed (req_id={req_id})");
110+
tdx.subscribe_quotes(&Contract::stock("AAPL"))?;
111+
println!("Subscribed to AAPL quotes");
109112

110113
// Block until shutdown
111114
std::thread::park();
115+
tdx.stop_streaming();
112116
Ok(())
113117
}
114118
```
@@ -237,7 +241,7 @@ async fn main() -> Result<(), thetadatadx::Error> {
237241
## Configuration
238242

239243
```rust
240-
use thetadatadx::DirectConfig;
244+
use thetadatadx::{ThetaDataDx, DirectConfig};
241245

242246
// Production (ThetaData NJ datacenter, gRPC over TLS)
243247
let config = DirectConfig::production();

crates/thetadatadx/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "thetadatadx"
3-
version = "2.0.0"
3+
version = "3.0.0"
44
edition = "2021"
55
rust-version = "1.85"
66
authors = ["userFRM"]

crates/thetadatadx/src/direct.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,13 @@ macro_rules! contract_spec {
202202
/// # Example
203203
///
204204
/// ```rust,no_run
205-
/// use thetadatadx::{DirectClient, Credentials, DirectConfig};
205+
/// use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
206206
///
207207
/// # async fn run() -> Result<(), thetadatadx::Error> {
208208
/// let creds = Credentials::from_file("creds.txt")?;
209-
/// let client = DirectClient::connect(&creds, DirectConfig::production()).await?;
209+
/// let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
210210
///
211-
/// let eod = client.stock_history_eod("AAPL", "20240101", "20240301").await?;
211+
/// let eod = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
212212
/// println!("{} EOD ticks", eod.len());
213213
/// # Ok(())
214214
/// # }
@@ -1906,7 +1906,7 @@ impl DirectClient {
19061906
/// # Example
19071907
///
19081908
/// ```rust,no_run
1909-
/// # async fn run(client: &thetadatadx::DirectClient) -> Result<(), thetadatadx::Error> {
1909+
/// # async fn run(client: &thetadatadx::direct::DirectClient) -> Result<(), thetadatadx::Error> {
19101910
/// use thetadatadx::proto_v3;
19111911
///
19121912
/// let request = proto_v3::CalendarYearRequest {

crates/thetadatadx/src/lib.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,53 @@
1818
//!
1919
//! ## Quick Start
2020
//!
21+
//! The recommended entry point is [`ThetaDataDx`], which authenticates once and
22+
//! provides both historical and streaming through a single object:
23+
//!
2124
//! ```rust,ignore
22-
//! use thetadatadx::{DirectClient, Credentials, DirectConfig};
25+
//! use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
26+
//! use thetadatadx::fpss::{FpssData, FpssControl, FpssEvent};
27+
//! use thetadatadx::fpss::protocol::Contract;
2328
//!
2429
//! #[tokio::main]
2530
//! async fn main() -> Result<(), thetadatadx::Error> {
2631
//! let creds = Credentials::from_file("creds.txt")?;
27-
//! let client = DirectClient::connect(&creds, DirectConfig::production()).await?;
2832
//!
29-
//! // Historical data (MDDS/gRPC)
30-
//! let ticks = client.stock_history_eod("AAPL", "20240101", "20240301").await?;
33+
//! // Connect -- authenticates once, historical ready immediately
34+
//! let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
35+
//!
36+
//! // Historical (MDDS gRPC) -- all 61 methods via Deref
37+
//! let ticks = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
38+
//!
39+
//! // Streaming (FPSS TCP) -- connects lazily on first call
40+
//! tdx.start_streaming(|event: &FpssEvent| {
41+
//! match event {
42+
//! FpssEvent::Data(FpssData::Trade { contract_id, price, size, .. }) => {
43+
//! println!("Trade: {contract_id} @ {price} x {size}");
44+
//! }
45+
//! _ => {}
46+
//! }
47+
//! })?;
3148
//!
32-
//! // Real-time streaming (FPSS/TCP)
33-
//! // let stream = client.subscribe_quotes("AAPL").await?;
49+
//! tdx.subscribe_quotes(&Contract::stock("AAPL"))?;
50+
//!
51+
//! // ... when done:
52+
//! tdx.stop_streaming();
3453
//! Ok(())
3554
//! }
3655
//! ```
3756
//!
57+
//! For historical-only usage, just skip `start_streaming()` -- all 61 historical
58+
//! methods are available directly on `ThetaDataDx` via `Deref<Target = DirectClient>`:
59+
//!
60+
//! ```rust,ignore
61+
//! use thetadatadx::{ThetaDataDx, Credentials, DirectConfig};
62+
//!
63+
//! let creds = Credentials::from_file("creds.txt")?;
64+
//! let tdx = ThetaDataDx::connect(&creds, DirectConfig::production()).await?;
65+
//! let ticks = tdx.stock_history_eod("AAPL", "20240101", "20240301").await?;
66+
//! ```
67+
//!
3868
//! ## Reverse-Engineering Notes
3969
//!
4070
//! This crate was built by decompiling ThetaData's Java terminal (v202603181, 58.5MB):
@@ -73,6 +103,7 @@ pub mod fpss;
73103
pub mod greeks;
74104
pub mod registry;
75105
pub mod types;
106+
pub mod unified;
76107

77108
/// Generated protobuf types from `endpoints.proto` (shared types).
78109
///
@@ -97,6 +128,6 @@ pub mod proto_v3 {
97128

98129
pub use auth::Credentials;
99130
pub use config::DirectConfig;
100-
pub use direct::DirectClient;
101131
pub use error::Error;
102132
pub use registry::{EndpointMeta, ParamMeta, ParamType, ReturnType, ENDPOINTS};
133+
pub use unified::ThetaDataDx;

0 commit comments

Comments
 (0)