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).
  • 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.

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.

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.

req_mkt_data_ex takes mkt_data_options=None after mode_9887, with the same option checks as req_mkt_data. With no session, both 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 callbacks

c.quote(req_id) returns a dict of bid, ask, last, bid_size, ask_size, last_size, volume, high, low, open, close for a running subscription, or None if that request id is not subscribed. No wrapper state to keep, no lock to take.

Run it

IB_USERNAME=... IB_PASSWORD=... python examples/hello_tick_data.py

Source

"""Recipe: stream SPY market data for 5 seconds, print latest bid/ask/last.

Usage:
    IB_USERNAME=... IB_PASSWORD=... python examples/hello_tick_data.py
"""

import os
import threading
import time

from ib_dx import EClient, EWrapper, Contract


class TickWrapper(EWrapper):
    def __init__(self):
        self.bid = 0.0
        self.ask = 0.0
        self.last = 0.0
        self.ticks = 0

    def tick_price(self, req_id, tick_type, price, attrib):
        self.ticks += 1
        if tick_type == 1:
            self.bid = price
        elif tick_type == 2:
            self.ask = price
        elif tick_type == 4:
            self.last = price

    def tick_size(self, req_id, tick_type, size):
        self.ticks += 1

    def error(self, req_id, error_time, code, msg, advanced=""):
        if code not in (2104, 2106, 2158):
            print(f"[error] {code}: {msg}")


w = TickWrapper()
c = EClient(w)
c.connect(
    username=os.environ["IB_USERNAME"],
    password=os.environ["IB_PASSWORD"],
    paper=True,
)
threading.Thread(target=c.run, daemon=True).start()

spy = Contract()
spy.con_id = 756733
spy.symbol = "SPY"
spy.sec_type = "STK"
spy.exchange = "SMART"
spy.currency = "USD"

req_id = 1
print("streaming SPY for 5s…")
c.req_mkt_data(req_id, spy, "", False, False)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
    time.sleep(0.1)
c.cancel_mkt_data(req_id)

print(f"ticks: {w.ticks}  bid={w.bid:.2f}  ask={w.ask:.2f}  last={w.last:.2f}")
c.disconnect()