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_l2 events 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:

fieldvalues
operation0 insert, 1 update, 2 delete
side0 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()