thetadatadx 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Node.js SDK for ThetaData market data. napi-rs bindings over the `thetadatadx` R
4
4
 
5
5
  Every call crosses the napi boundary into compiled Rust: gRPC, protobuf, zstd, FIT decoding, and TCP streaming run inside the `thetadatadx` crate.
6
6
 
7
- > **FLATFILES coverage:** the TypeScript binding currently exposes the MDDS (historical) and FPSS (streaming) surfaces only. The third surface — FLATFILES whole-universe daily blobs is shipped in the Rust core (v8.0.17+) and is being wired into TypeScript under issue [#436](https://github.com/userFRM/ThetaDataDx/issues/436). See [`ROADMAP.md`](../../ROADMAP.md#flatfiles--binding-coverage) for the per-binding status.
7
+ > **Surface coverage:** the TypeScript binding exposes all three ThetaData surfaces — MDDS (historical), FPSS (streaming), and FLATFILES (whole-universe daily blobs). Flat files land via `tdx.flatFiles.*()` with `.toArrowIpc()` and `.toJson()` terminals plus a `tdx.flatFileToPath(...)` raw-bytes helper see the [Flat Files](#flat-files) section for the full method list.
8
8
 
9
9
  ## Install
10
10
 
@@ -20,12 +20,12 @@ Pre-built binaries are downloaded automatically for your platform. Supported:
20
20
  ## Usage
21
21
 
22
22
  ```js
23
- const { ThetaDataDx } = require('thetadatadx');
23
+ const { ThetaDataDxClient } = require('thetadatadx');
24
24
 
25
25
  async function main() {
26
26
  // Connect (requires ThetaData credentials)
27
- const tdx = ThetaDataDx.connectFromFile('creds.txt');
28
- // Or: const tdx = ThetaDataDx.connect('user@example.com', 'password');
27
+ const tdx = ThetaDataDxClient.connectFromFile('creds.txt');
28
+ // Or: const tdx = ThetaDataDxClient.connect('user@example.com', 'password');
29
29
 
30
30
  // Historical endpoints return an array of typed tick objects
31
31
  // (`OhlcTick[]`, `QuoteTick[]`, ...). Index into the array to
@@ -36,25 +36,90 @@ async function main() {
36
36
  // With timeout
37
37
  const snap = tdx.stockSnapshotQuote(['AAPL', 'MSFT'], null, null, 5000);
38
38
 
39
- // Streaming — register a callback. napi-rs `ThreadsafeFunction`
40
- // routes every event through libuv's `uv_async_t` queue onto the
41
- // Node main thread; the callback runs there, decoupled from the
42
- // FPSS reader thread. A slow callback fills the SSOT dispatcher's
43
- // bounded queue and overflow events are dropped (observable via
44
- // `tdx.droppedEventCount()`); the FPSS TLS reader is never blocked.
45
- tdx.startStreaming((event) => {
39
+ // Streaming — primary fluent contract-first API.
40
+ // `Contract.stock("AAPL").quote()` returns a typed `Subscription`
41
+ // value the polymorphic `client.subscribe(...)` accepts directly,
42
+ // matching the documented Rust / Python shape.
43
+ const stock = ContractRef.stock('AAPL');
44
+ const option = ContractRef.option('SPY', '20260620', '550', 'C');
45
+
46
+ await using session = await tdx.streaming((event) => {
46
47
  if (event.kind === 'quote') {
47
48
  console.log(event.quote.bid, event.quote.ask);
48
49
  }
49
50
  });
50
- tdx.subscribeQuotes('AAPL');
51
+ tdx.subscribe(stock.quote());
52
+ tdx.subscribe(option.trade());
53
+ tdx.subscribe(SecType.option().fullTrades());
54
+ tdx.subscribeMany([stock.quote(), option.quote()]);
51
55
  // ...do other work; the callback fires on incoming events...
52
- tdx.stopStreaming();
56
+ // `[Symbol.asyncDispose]` runs on scope exit:
57
+ // stopStreaming(); await awaitDrain(5000);
58
+ // If the drain barrier times out, `console.warn` fires but the
59
+ // `using` scope still exits cleanly.
53
60
  }
54
61
 
55
62
  main().catch(console.error);
56
63
  ```
57
64
 
65
+ The `await using` form is the recommended API. The `StreamingSession`
66
+ forwards every method call (`subscribe`, `subscribeMany`,
67
+ `unsubscribe`, `unsubscribeMany`, `activeSubscriptions`,
68
+ `droppedEventCount`, `reconnect`, ...) to the underlying `ThetaDataDxClient`
69
+ through a `Proxy`, so adding a new method to the napi binding makes it
70
+ callable through the session automatically -- the streaming surface is
71
+ a single source of truth rooted in the Rust crate.
72
+
73
+ For the lower-level escape hatch (e.g. cross-process lifecycle
74
+ management, custom shutdown ordering), call the lifecycle methods
75
+ explicitly:
76
+
77
+ ```ts
78
+ tdx.startStreaming((event) => { /* ... */ });
79
+ tdx.subscribe(ContractRef.stock('AAPL').quote());
80
+ // ...do other work...
81
+ tdx.stopStreaming();
82
+ // Drain barrier: by the time `awaitDrain(5000)` resolves, the
83
+ // consumer thread is guaranteed to have finished firing the
84
+ // callback, so the JS closure can be released without a
85
+ // use-after-free race against the LMAX Disruptor consumer.
86
+ const drained = await tdx.awaitDrain(5000);
87
+ if (!drained) console.warn('drain timed out');
88
+ ```
89
+
90
+ ### Pull-iter delivery — `for await (const event of iter)` (high-throughput drain)
91
+
92
+ Push-callback (`tdx.streaming(callback)` above) is the recommended
93
+ default for low-latency single-event reaction. Pull-iter is the
94
+ sibling delivery mode for high-throughput batch processing where
95
+ the dominant cost is per-event JS work rather than per-event vendor
96
+ latency:
97
+
98
+ ```ts
99
+ const iter = tdx.startStreamingIter();
100
+ tdx.subscribe(SecType.option().fullTrades());
101
+ for await (const event of iter) {
102
+ if (event.kind === 'trade') {
103
+ buf.push([event.trade.price, event.trade.size]);
104
+ }
105
+ }
106
+ // Stop the streaming session when done. The async-iterator
107
+ // protocol handles `break` cleanly via `return()`, which calls
108
+ // `iter.close()` so the worker thread stops blocking on the queue.
109
+ tdx.stopStreaming();
110
+ await tdx.awaitDrain(5000);
111
+ ```
112
+
113
+ The Disruptor consumer pushes events into a per-client bounded
114
+ queue; the `for await` loop drains the queue from the Node main
115
+ thread in batches, with the actual queue wait happening on
116
+ `tokio::task::spawn_blocking` so the event loop is never blocked.
117
+
118
+ Mode is chosen at start. Push and pull are mutually exclusive on a
119
+ given client; switch by calling `stopStreaming()` first. Backpressure
120
+ surfaces on the same `droppedEventCount()` counter as the callback
121
+ path.
122
+
58
123
  ## TypeScript types
59
124
 
60
125
  Every tick type and FPSS event is emitted as a `#[napi(object)]` struct on
@@ -72,22 +137,50 @@ discriminated `FpssEvent`, narrowed on `event.kind`:
72
137
  ```ts
73
138
  tdx.startStreaming((event: FpssEvent) => {
74
139
  switch (event.kind) {
75
- case 'quote': /* event.quote is Quote */ break;
76
- case 'trade': /* event.trade is Trade */ break;
77
- case 'ohlcvc': /* event.ohlcvc is Ohlcvc */ break;
78
- case 'open_interest': /* event.openInterest is OpenInterest */ break;
79
- case 'simple': /* event.simple is FpssSimplePayload */ break;
80
- case 'raw_data': /* event.rawData is FpssRawDataPayload */ break;
140
+ // Market-data ticks
141
+ case 'quote': /* event.quote is Quote */ break;
142
+ case 'trade': /* event.trade is Trade */ break;
143
+ case 'ohlcvc': /* event.ohlcvc is Ohlcvc */ break;
144
+ case 'open_interest': /* event.openInterest is OpenInterest */ break;
145
+
146
+ // Control / lifecycle events — one typed payload per FpssControl variant
147
+ case 'login_success': /* event.loginSuccess is LoginSuccess */ break;
148
+ case 'contract_assigned': /* event.contractAssigned is ContractAssigned */ break;
149
+ case 'req_response': /* event.reqResponse is ReqResponse */ break;
150
+ case 'market_open': /* event.marketOpen is MarketOpen */ break;
151
+ case 'market_close': /* event.marketClose is MarketClose */ break;
152
+ case 'server_error': /* event.serverError is ServerError */ break;
153
+ case 'disconnected': /* event.disconnected is Disconnected */ break;
154
+ case 'reconnecting': /* event.reconnecting is Reconnecting */ break;
155
+ case 'reconnected': /* event.reconnected is Reconnected */ break;
156
+ case 'error': /* event.error is Error */ break;
157
+ case 'unknown_frame': /* event.unknownFrame is UnknownFrame */ break;
158
+ case 'connected': /* event.connected is Connected */ break;
159
+ case 'ping': /* event.ping is Ping */ break;
160
+ case 'reconnected_server': /* event.reconnectedServer is ReconnectedServer */ break;
161
+ case 'restart': /* event.restart is Restart */ break;
162
+ case 'unknown_control': /* event.unknownControl is UnknownControl */ break;
81
163
  }
82
164
  });
83
165
  ```
84
166
 
85
- The `kind` field is typed as the string-literal union
86
- `'ohlcvc' | 'open_interest' | 'quote' | 'trade' | 'simple' | 'raw_data'`
87
- plain strings, not a TS `enum` (the previous `const enum FpssEventKind`
88
- was removed in #376 because it broke downstream consumers with
89
- `"isolatedModules": true`), so it works in every toolchain
90
- including Vite, esbuild, ts-jest, and Next.js.
167
+ Truncated / unrecognised wire frames are filtered before the callback
168
+ fires and accounted on the `thetadatadx.fpss.decode_failures` metric
169
+ counter on the Rust side; they never surface as an `FpssEvent`.
170
+
171
+ The `kind` field is typed as a string-literal union narrowed by the
172
+ generated `index.d.ts` plain strings, not a TS `enum` (the previous
173
+ `const enum FpssEventKind` was removed in #376 because it broke
174
+ downstream consumers with `"isolatedModules": true`), so it works in
175
+ every toolchain including Vite, esbuild, ts-jest, and Next.js.
176
+
177
+ Each typed control payload mirrors the corresponding `FpssControl::*`
178
+ Rust variant one-for-one — `Disconnected.reason` / `Reconnecting.reason`
179
+ carry the `RemoveReason` discriminant as `i32`, `Reconnecting.delayMs` is
180
+ `bigint` (`u64`), `Ping.payload` and `UnknownFrame.payload` are
181
+ `Buffer`-backed byte arrays. Both Python and TypeScript surfaces are
182
+ generated from `fpss_event_schema.toml`, so consumer code ports between
183
+ the two languages without a discriminator rewrite.
91
184
 
92
185
  ### `bigint` fields
93
186
 
@@ -98,12 +191,40 @@ OHLC / EOD tick, `droppedEventCount()` on the streaming client, and
98
191
  (`42n`) for comparisons or widen to `Number(x)` at the point of
99
192
  display (watch for loss of precision beyond 2^53).
100
193
 
101
- `FpssSimplePayload.eventType` carries the concrete control-event name
102
- (`"login_success"`, `"contract_assigned"`, `"disconnected"`,
103
- `"market_open"`, `"market_close"`, ...). The wire tag set matches the
104
- Python SDK's `next_event` pyclasses byte-for-byte both surfaces are
105
- generated from `fpss_event_schema.toml`, so consumer code ports between
106
- the two languages without a discriminator rewrite.
194
+ ## Flat Files
195
+
196
+ Whole-universe daily snapshots over the legacy MDDS port. Decoded schema
197
+ is determined at runtime by `(SecType, ReqType)`, so the binding emits
198
+ Arrow IPC stream bytes pair with `apache-arrow`'s `tableFromIPC` to
199
+ materialise a typed `Table`.
200
+
201
+ ```ts
202
+ import { ThetaDataDxClient } from "thetadatadx";
203
+ import { tableFromIPC } from "apache-arrow"; // peer dep
204
+
205
+ const tdx = ThetaDataDxClient.connectFromFile("creds.txt");
206
+
207
+ const rows = tdx.flatFiles.optionQuote("20260428");
208
+ console.log(rows.len());
209
+
210
+ const ipc = rows.toArrowIpc();
211
+ const table = tableFromIPC(ipc);
212
+
213
+ // Or skip Arrow and emit a JSON array of objects
214
+ const json = JSON.parse(rows.toJson());
215
+
216
+ // Generic dispatcher
217
+ const oi = tdx.flatFiles.request("OPTION", "OPEN_INTEREST", "20260428");
218
+
219
+ // Raw vendor CSV / JSONL straight to disk
220
+ tdx.flatfileToPath("OPTION", "QUOTE", "20260428",
221
+ "/tmp/option-quote", "csv");
222
+ ```
223
+
224
+ Available `flatFiles.*` methods: `optionQuote`, `optionTrade`,
225
+ `optionTradeQuote`, `optionOhlc`, `optionOpenInterest`, `optionEod`,
226
+ `stockQuote`, `stockTrade`, `stockTradeQuote`, `stockEod`, plus
227
+ `request(secType, reqType, date)`.
107
228
 
108
229
  ## Building from source
109
230
 
@@ -117,7 +238,7 @@ npm run build # requires Rust stable + protoc
117
238
 
118
239
  ## API reference
119
240
 
120
- Every historical endpoint from `endpoint_surface.toml` is exposed as a camelCase method on `ThetaDataDx`. See `index.d.ts` for the complete method list with JSDoc comments.
241
+ Every historical endpoint from `endpoint_surface.toml` is exposed as a camelCase method on `ThetaDataDxClient`. See `index.d.ts` for the complete method list with JSDoc comments.
121
242
 
122
243
  ## Docs
123
244