Streaming Ticks

Subscribe to top-of-book on SPY, collect for five seconds, print the latest bid, ask and last.

What this shows

  • req_mkt_data(req_id, &contract, generic_tick_list, snapshot, regulatory_snapshot, mkt_data_options).
  • Reading prices off tick_price, where tick_type names which price it is: 1 bid, 2 ask, 4 last, 9 close.
  • cancel_mkt_data(req_id) before disconnecting.

What comes back

tick_req_params (tickReqParams in Python) states the contract's price increment, exchange and snapshot permission once per request, with the first record the request is served and ahead of its ticks, as a gateway states it; a request served no record is told nothing. It reads the latest stored parameters when the callback is delivered. Where the venue names no best-bid-and-offer exchange, as for a currency, a crypto or a bond, the exchange is empty. The permission is 0 there, on a bond, a bill, a fixed-income contract or a combination, and while the frozen quote is served in place of the live one, whatever the venue stated, as a gateway states it. On a closed market whose frozen quote is served, that is the record the parameters go out with, so the permission is 0. A follower sharing the subscription gets its own callback; cancelling and reusing its request id starts a new request. A request cancelled before delivery receives no callback.

tick_price and tick_size for as long as the subscription runs.

tick_generic fires for the halt state the venue states on tick 49: 0 while the contract is trading, 1 once it has stopped.

With snapshot: true you get the first available quote, then tick_snapshot_end, and this client cancels the subscription for you. That is this client ending a subscription, not a separate request.

regulatory_snapshot: true is the venue's own one-shot snapshot. It is a different request type, and an account without the entitlement is refused by the venue. It also ends on tick_snapshot_end.

Limits

Each number in generic_tick_list is asked for, as a subscription of its own beside the prices. The number you state is the number the venue knows the series by, so there is nothing to translate. "292" additionally subscribes to news for that contract, and "292:BRFG+DJNL" asks for it from the providers it names; an entry that is not a number is reported rather than sent.

Every number the protocol will accept in that list is read here, and each arrives under the number the reference client publishes it under:

you ask foryou receive
100call and put option volume, on tick_size 29 and 30
101call and put open interest, on tick_size 27 and 28
104 512historical volatility, on tick_generic 23
105average option volume, the two sides added, on tick_size 87
106option implied volatility, on tick_generic 24
162the premium of an index over the future written on it, on tick_generic 31
165the day's volume and the 13, 26 and 52-week extremes, on tick_size 21 and tick_price 15 to 20
220the mark the venue keeps, on tick_price 78
221 232the mark, on tick_price 37
225auction volume and imbalance on tick_size 34 and 36, auction price on tick_price 35, the regulatory imbalance on tick_size 61
233the trade tape, on tick_string 48: price;size;time;volume;vwap;single
236shortability on tick_generic 46, and the borrowable share count on tick_size 89
258company ratios, on tick_string 47
292 292:BRFG+DJNLnews, on tick_news
293 294 295trade count, trade rate and volume rate, on tick_generic 54, 55 and 56
318last regular-session trade, on tick_price 57
375the trade-report tape, on tick_string 77
411real-time historical volatility, on tick_generic 58
456what the contract pays out, on tick_string 59
460bond factor multiplier, on tick_generic 60
499borrow fee rate, on tick_price 111
577 623an ETF's net asset value, last and frozen, on tick_price 96 and 97
586the estimated IPO midpoint on tick_generic 101, and what it opened at on 102
588futures open interest, on tick_size 86
595the 3, 5 and 10-minute volumes, on tick_size 63, 64 and 65
614an ETF's net asset value high and low, on tick_price 98 and 99
619the slow mark, on tick_price 79
787the odd lot: both prices on tick_price 105 and 106, their sizes on tick_size 107 and 108, where each is quoted on tick_string 109 and 110

Any other number is asked for the same way, as a series of its own. What the venue states on series the TWS API has no tick for is read through the calls under Beyond the documented API.

One subscription per contract. To change the mode on a contract, cancel first.

Every subscription starts live, whatever the type set on req_market_data_type. As a gateway takes the types, 2 turns frozen data on, 3 and 4 turn delayed data on (4 with delayed-frozen, 3 without), and only 1 turns frozen data off, so a 2 asked after a 3 or a 4 still leaves the delayed feed to fall back to. With delayed data on, a bid/ask refusal that states delayed data is available switches to the delayed feed and reports 10167 without ending the request, and market_data_type reports 3. With nothing to fall back to, the refusal ends the request as a gateway ends it: under 354, "Requested market data is not subscribed.", followed by "Delayed market data is available." where the refusal says so; or under 10089, where the services the refusal names for the API reach past those it names for the data. The venue's own words follow either, as sent. Under type 2 alone the request ends there, and no frozen quote is asked for.

Where the logon enables frozen data, the market's status is watched, as a gateway watches it, for a subscription made with frozen data on while it is served live, and for one that has fallen back to delayed data with delayed-frozen data on. While the status says the market is closed, the frozen quote is asked for beside the live one and served, reported as 2; beside a delayed one the delayed-frozen quote is asked for too, and served from its first record, reported as 4, the delayed one until then. The quote asked for first is kept up meanwhile, and once the status says the market is open the frozen quotes are withdrawn and that quote is served again as it stands, reported as 1 or 3. A refusal of the live quote ends the watch on it without a word: the frozen quote beside it is withdrawn, and so is the status, unless the fallback watches it. So a login entitled to live data is served live under type 4 alone, and on a closed market its quote states what the live feed states, zero yields included; under type 2, alone or before a 4, it is served the frozen quote. A calculation's own subscription falls back to nothing: it asks the delayed feed directly where delayed data is on, and watches no status.

req_mkt_data_ex selects a feed directly: mode_9887 is 0 realtime, 1 delayed, 2 frozen, or 3 delayed-frozen. Frozen keeps thinly traded names quoting after hours, when the realtime feed is silent.

A refusal naming several requests reaches each affected caller. The bid/ask and last entries of one quote produce one subscription error. Cancelling a tick-by-tick stream uses its query identifier before acknowledgement and its assigned stream identifier afterwards; another caller sharing that stream keeps receiving data until it cancels too.

Both quote requests take mkt_data_options: &[TagValue], in req_mkt_data_ex after mode_9887. Pass &[] when there are no options. With no session, both calls report 504, Not connected, before checking the request.

The list accepts manual=0 or manual=1. An unknown key is refused under 10337, an invalid value under 10338, and a malformed list under 320. NOAPIMISCVLD at logon lifts the key and value checks; the later check that manual is zero or one remains.

Reading without the callback loop

quote(req_id) returns the latest bid, ask, last and sizes for a running subscription, with no wrapper and no lock. quote_of(&contract) does the same by contract. Both return None until the first tick has landed.

Prices and sizes on Quote are integers scaled by PRICE_SCALE and QTY_SCALE (both 108). Divide when you display; the tick_price callback hands you f64 already.

Run it

IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_tick_data

Source

//! Recipe: stream SPY market data for 5 seconds, print latest bid/ask/last.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_tick_data

use std::env;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use ib_dx::api::client::{Contract, EClient, EClientConfig};
use ib_dx::api::types::TickAttrib;
use ib_dx::api::wrapper::Wrapper;

#[derive(Default)]
struct State {
    bid: f64,
    ask: f64,
    last: f64,
    ticks: u64,
}

struct TickWrapper {
    state: Arc<Mutex<State>>,
}

impl Wrapper for TickWrapper {
    fn tick_price(&mut self, _req_id: i64, tick_type: i32, price: f64, _: &TickAttrib) {
        let mut s = self.state.lock().unwrap();
        s.ticks += 1;
        match tick_type {
            1 => s.bid = price,
            2 => s.ask = price,
            4 => s.last = price,
            _ => {}
        }
    }
    fn error(&mut self, req_id: i64, _error_time: i64, code: i64, msg: &str, _adv: &str) {
        if !matches!(code, 2104 | 2106 | 2158) {
            eprintln!("[error] req_id={req_id} code={code} msg={msg}");
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = EClient::connect(&EClientConfig {
        username: env::var("IB_USERNAME")?,
        password: env::var("IB_PASSWORD")?,
        paper: true,
        ..Default::default()
    })?;

    let state = Arc::new(Mutex::new(State::default()));
    let mut wrapper = TickWrapper { state: state.clone() };

    let spy = Contract {
        con_id: 756733,
        symbol: "SPY".into(),
        sec_type: "STK".into(),
        exchange: "SMART".into(),
        currency: "USD".into(),
        ..Default::default()
    };

    let req_id = 1;
    println!("streaming SPY for 5s…");
    client.req_mkt_data(req_id, &spy, "", false, false, &[]);

    let deadline = Instant::now() + Duration::from_secs(5);
    while Instant::now() < deadline {
        client.process_msgs(&mut wrapper);
        std::thread::sleep(Duration::from_millis(20));
    }

    client.cancel_mkt_data(req_id);

    let s = state.lock().unwrap();
    println!("ticks: {}  bid={:.2}  ask={:.2}  last={:.2}", s.ticks, s.bid, s.ask, s.last);

    drop(s);
    client.disconnect();
    Ok(())
}