Skip to content

Streaming

Real-time quotes, trades, and open interest are delivered as typed events through a callback you register once. The same client that serves market-data requests runs the streaming session: connect, start streaming, subscribe.

Streaming requires a Standard subscription or higher on the matching asset class — see Subscriptions. Markets closed? Connect with the dev() configuration to stream a replayed session.

Streaming authenticates the same way as market-data requests. An API key works here too: set THETADATA_API_KEY and build credentials with from_env_or_file (or the api-key constructor) in place of from_file. See Authenticate.

Delivery modes

The same subscriptions can be consumed two ways:

  • Per-event callback (start_streaming): each event is pushed to a callback you register, one at a time. Reach for it when you react to events as they arrive and want the lowest per-event latency.
  • Columnar pull (batches): events are pulled as Apache Arrow RecordBatch values under a fixed schema. Reach for it when you want bulk, columnar throughput into pandas, polars, or DuckDB.

The callback path is documented below; the pull reader is covered under Columnar batches. Pick one per session, not both.

Connect, subscribe, receive

rust
use thetadatadx::streaming::Contract;
use thetadatadx::streaming::{StreamData, StreamEvent};
use thetadatadx::{Credentials, DirectConfig, Client};

async fn run() -> Result<(), thetadatadx::Error> {
    let creds = Credentials::from_file("creds.txt")?;
    let client = Client::connect(&creds, DirectConfig::production()).await?;

    client.stream().start_streaming(|event: &StreamEvent| {
        if let StreamEvent::Data(StreamData::Quote { contract, bid, ask, .. }) = event {
            println!("{} bid={bid} ask={ask}", contract.symbol);
        }
    })?;

    client.stream().subscribe(Contract::stock("AAPL").quote())?;
    std::thread::sleep(std::time::Duration::from_secs(60));

    client.stream().stop_streaming();
    Ok(())
}

The callback runs on a dedicated consumer thread — no async executor between the wire and your code. subscribe / unsubscribe are callable from any thread.

Subscriptions

Build a typed subscription from a Contract (per-contract scope) or a SecType (full-stream scope), then pass it to subscribe / unsubscribe:

  • Per contract: Contract.stock("AAPL"), Contract.option("SPY", { expiration: "20260618", strike: "570", right: "C" }), Contract.index("SPX") — then .quote(), .trade(), or .open_interest(). The option leg is named (a keyword argument in Python, an options object in TypeScript, a struct in Rust / C++) so a swapped expiration/strike/right cannot pass silently.
  • Full stream (every contract of a security type, stocks and options only): SecType + .full_trades() / .full_open_interest().
  • subscribe_many([...]) installs a batch in one call; active_subscriptions() snapshots what is installed.

The per-stream-type pages in the sidebar carry the exact subscribe code, the event fields, and the unsubscribe call for each stream.

Columnar batches

client.stream().batches(...) opens a pull reader that delivers the same subscriptions as Apache Arrow RecordBatch values under a fixed schema, a sibling to the per-event callback. The reader is iterable in each binding (synchronous and async), yields batches you concatenate freely, and tears the session down on close (context-manager exit, Symbol.asyncDispose, or the destructor). Open the reader first, since it starts the session, then subscribe.

Three knobs tune it:

  • batch_size: rows per batch. A batch is emitted when it fills or when linger elapses, whichever comes first.
  • linger: the maximum time a partial batch waits before being emitted, so a quiet stream still delivers.
  • backpressure: what happens when the reader falls behind. Block (the default, lossless: the wire is paced) or DropOldest (a bounded buffer of capacity batches that drops, and counts, the oldest on overflow).

The field set is the fixed streaming schema, shared across bindings.

rust
use thetadatadx::streaming::Contract;
use thetadatadx::{Credentials, DirectConfig, Client};
use futures::StreamExt;

async fn run() -> Result<(), thetadatadx::Error> {
    let creds = Credentials::from_file("creds.txt")?;
    let client = Client::connect(&creds, DirectConfig::production()).await?;

    // `batches()` starts the session, so open the reader first, then subscribe.
    let mut batches = client.stream().batches().batch_size(8_192).build()?;
    client.stream().subscribe(Contract::stock("AAPL").trade())?;

    while let Some(batch) = batches.next().await {
        println!("{} rows", batch?.num_rows());
    }
    Ok(())
}

build() returns a RecordBatchStream that implements futures::Stream; call .next_blocking() to pull batches synchronously (one per call) instead. Dropping it (or close()) tears the session down.

Lifecycle

  • Start once, subscribe many. client.stream.start_streaming(callback) opens the session; subscriptions attach and detach freely afterwards.
  • Stopping. client.stream.stop_streaming() closes the session and clears the callback (in C++, the destructor does this). client.stream.await_drain(timeout_ms) blocks until queued events have been delivered.
  • Reconnects are automatic with resubscription of everything you had installed; policy and monitoring live in Reconnection & Monitoring.
  • Event order is per-connection arrival order; every data event carries received_at_ns, the local receive timestamp.

Released under the Apache-2.0 License.