Streaming L2 Market Depth
Subscribe to Level-2 depth on two contracts at once, keep a bid and ask book per contract from the update stream, and print top-of-book at the end.
What this shows
req_mkt_depth(req_id, contract, num_rows=5), one request id per contract.- Applying
update_mkt_depth_l2events to a book keyed by level position. cancel_mkt_depth(req_id, is_smart_depth)for each subscription before disconnecting, naming the mode the book was asked in.
What comes back
update_mkt_depth_l2(req_id, position, market_maker, operation, side, price, size, is_smart_depth) on every level change:
| field | values |
|---|---|
operation | 0 insert, 1 update, 2 delete |
side | 0 ask, 1 bid |
A level carries the maker the venue names on it. An exchange-level book often
names none — many venues quote no makers — and then the field is empty; on the
aggregated book the exchange stands in where the venue names no maker. That is
why the example keeps market_maker alongside price and size.
The example collects for the whole window and prints once at the end, not on
every update. DURATION_SECS sets that window; the default is 15.
After a reconnect the venue restates the book from the top. The client reports
that on error as 317, the TWS API's notice that a book was reset, before the
first new level arrives — as a gateway does. Empty the book on it, or the
levels that follow land on top of the old ones.
Limits
A depth subscription the venue will not serve is refused, and the refusal
arrives on the error callback. The example prints an em dash for a side with
no levels rather than failing, so a book that never started shows up as zero
updates rather than as an error.
A contract whose security type and exchange are both left off is sent as it stands, and an unnamed exchange is read as the smart destination. Naming a security type that does not match the contract asks for one book as another's, which the venue refuses.
Run it
IB_USERNAME=... IB_PASSWORD=... python examples/hello_l2.py
Collect for longer:
DURATION_SECS=30 python examples/hello_l2.py
Source
"""Recipe: stream L2 market depth on AAPL + TSLA, print top-of-book on update.
Usage:
IB_USERNAME=... IB_PASSWORD=... python examples/hello_l2.py
"""
import os
import threading
import time
from ib_dx import EClient, EWrapper, Contract
class Book:
def __init__(self, symbol):
self.symbol = symbol
self.bids = {} # position -> (price, size, mm)
self.asks = {}
self.updates = 0
def apply(self, position, mm, operation, side, price, size):
self.updates += 1
book = self.bids if side == 1 else self.asks
if operation in (0, 1):
book[position] = (price, size, mm)
elif operation == 2:
book.pop(position, None)
def top(self):
bid = max(self.bids.values(), default=None, key=lambda v: v[0])
ask = min(self.asks.values(), default=None, key=lambda v: v[0])
return bid, ask
class L2Wrapper(EWrapper):
def __init__(self, books):
self.books = books # req_id -> Book
def update_mkt_depth_l2(self, req_id, position, market_maker,
operation, side, price, size, is_smart_depth):
if req_id in self.books:
self.books[req_id].apply(position, market_maker, operation, side, price, size)
def error(self, req_id, error_time, code, msg, advanced=""):
if code not in (2104, 2106, 2158):
print(f"[error] {code}: {msg}")
def make_stk(con_id, symbol):
c = Contract()
c.con_id = con_id
c.symbol = symbol
c.sec_type = "STK"
c.exchange = "SMART"
c.currency = "USD"
return c
SUBSCRIPTIONS = [
(1, make_stk(265598, "AAPL")),
(2, make_stk(76792991, "TSLA")),
]
DURATION_SECS = int(os.environ.get("DURATION_SECS", "15"))
books = {req_id: Book(c.symbol) for req_id, c in SUBSCRIPTIONS}
w = L2Wrapper(books)
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()
for req_id, contract in SUBSCRIPTIONS:
c.req_mkt_depth(req_id, contract, num_rows=5)
print(f"streaming L2 for {DURATION_SECS}s…")
time.sleep(DURATION_SECS)
for req_id, _ in SUBSCRIPTIONS:
c.cancel_mkt_depth(req_id)
for book in books.values():
bid, ask = book.top()
bid_s = f"{bid[0]:.2f} x {bid[1]:.0f}" if bid else "—"
ask_s = f"{ask[0]:.2f} x {ask[1]:.0f}" if ask else "—"
print(f" {book.symbol:<5} updates={book.updates:>5} bid={bid_s:>14} ask={ask_s:<14}")
c.disconnect()