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