# TWS API market depth: reqMktDepth and SMART depth in practice

> TWS API market depth explained: reqMktDepth, SMART depth, keeping a Level 2 book from updateMktDepthL2, error 317 and 309, with Python code.

Source: https://scalpeltrade.com/blog/tws-api-market-depth
By Scalpel Team. Published 2026-05-14, last changed 2026-10-22. 10 min read.

**In short:**

TWS API market depth comes from `reqMktDepth(reqId, contract, numRows, isSmartDepth, mktDepthOptions)`. Set `isSmartDepth` to True on a SMART-routed stock and IBKR merges every venue you subscribe to into one book, sent as `updateMktDepthL2` messages that insert, update or delete a numbered row on the bid or ask. You rebuild the book yourself from those messages, and empty it when error 317 says IBKR reset it. A default account can stream only 3 symbols' depth at once, and you need a depth subscription such as Nasdaq TotalView for rows to arrive.

Level 2 is the reason most traders want the API, and the part of the TWS API with the least forgiving rules. We stream it on every desk in Scalpel, so this is the version of IBKR's documentation we wish we had read first: each parameter, keeping a book correct from a stream of row edits, the limits, and how our montage is built from it. Everything about the API was checked against IBKR's reference and its 10.51.01 Python client on October 22, 2026. Everything about Scalpel comes from its source code. Scalpel is independent and not affiliated with or endorsed by Interactive Brokers.

## How TWS API market depth works: reqMktDepth

IBKR's [market depth reference](https://www.interactivebrokers.com/docs/tws-api/doc/md/market-depth) defines one request and two callbacks. Python, Java and C++ call the request `reqMktDepth`; C# and Visual Basic call it `reqMarketDepth`.

| Parameter | What it does |
| --- | --- |
| `reqId` | Your number for this book. Every update carries it, and you cancel with it |
| `contract` | The stock. For SMART depth its exchange must be `SMART` |
| `numRows` | How many rows to keep on each side |
| `isSmartDepth` | True: one book merged across venues. False: one venue's book |
| `mktDepthOptions` | Internal use. Pass an empty list |

Each update arrives as `updateMktDepthL2(reqId, position, marketMaker, operation, side, price, size, isSmartDepth)`. `side` is 1 for the bid and 0 for the ask. `position` is the row number, 0 at the inside. `operation` says what to do with that row: 0 insert, 1 update, 2 delete. With SMART depth, `marketMaker` is the exchange holding the order, such as NSDQ or ARCA; IBKR's montage shows venues, not firms ([MMID](https://scalpeltrade.com/guides/glossary#mmid)). Cancel with `cancelMktDepth(reqId, isSmartDepth)`.

One thing IBKR says plainly and many first versions miss: depth "is sent without sampling or filtering". The ordinary Level 1 quote is the opposite. IBKR's [streaming data page](https://www.interactivebrokers.com/docs/tws-api/doc/md/streaming-data) lists US stock quotes as snapshots every 250 ms. So the book can show a level being hit before your Level 1 last price moves, and your code must apply every message, because each one edits the row the previous one left.

## SMART depth vs direct depth in the IBKR API

| | SMART depth | Direct depth |
| --- | --- | --- |
| Request | `isSmartDepth=True`, contract on `SMART` | `isSmartDepth=False`, contract on one exchange |
| What you get | Every venue you subscribe to, in one book | That one exchange's book |
| `marketMaker` | The exchange code | The market maker's MPID |
| Like in TWS | BookTrader | The Market Depth window |

For a scalper, SMART depth is the useful one: it is the whole lit book in one stream, and it costs one depth slot for the symbol however many venues feed it. Which venues appear depends on your subscriptions. Nasdaq TotalView and NYSE ArcaBook are the usual pair for small caps; our guide to [IBKR market data for day trading](https://scalpeltrade.com/guides/ibkr-market-data-for-day-trading) walks through subscribing, and [Nasdaq TotalView](https://scalpeltrade.com/blog/nasdaq-totalview) covers what that feed adds.

Two contradictions in IBKR's own material, so you do not lose an afternoon to them:

- The reference page's "Receive Market Depth" summary pairs SMART depth with `updateMktDepth` and direct depth with `updateMktDepthL2`, yet the same page describes `updateMktDepth` as "used for direct routed requests only" and gives `updateMktDepthL2` a `marketMaker` field that holds "the exchange holding the order if isSmartDepth is True". The safe answer is to implement both callbacks and route them through one function, which is what the code below and Scalpel both do.
- The docstring inside IBKR's Python client still says depth "must be direct-routed to an exchange and not smart-routed". The `isSmartDepth` flag and the reference page say otherwise, and SMART depth works.

To see what direct depth looks like before you code it, IBKR's lesson on the TWS Market Depth window is a nine-minute tour, including the per-exchange buttons that mirror direct requests:

*Video: [TWS Market Depth Trader (Level II)](https://www.youtube.com/watch?v=J00sF18Xd6s) (Interactive Brokers, 8:51)*

## Building the book from updateMktDepthL2

The book is a list per side, best first, and every message edits one numbered row. The rule that matters: an insert at a position pushes the rows below it down one, and a delete pulls them up one. Treat an insert as an overwrite and you lose a row; skip the shift on a delete and the rest of that side is off by one until the next reset.

![Four snapshots of a bid side as IBKR depth messages arrive: an insert at row 1 pushes three rows down, a delete at row 3 pulls the last row up, and an update at row 0 changes a size in place.](https://scalpeltrade.com/images/blog/tws-api-market-depth/row-operations.webp)

*Three depth messages applied to one bid side. The insert and the delete move every row below them, which is why each message must be applied in order to the book the previous one left.*

This Python script keeps both sides correct and prints the top three rows a second for ten seconds:

```python
import threading
import time

from ibapi.client import EClient
from ibapi.contract import Contract
from ibapi.wrapper import EWrapper

REQ_ID, ROWS = 2001, 10

class DepthApp(EWrapper, EClient):
    def __init__(self):
        EClient.__init__(self, self)
        self.ready = threading.Event()
        self.bids, self.asks = [], []  # rows of [venue, price, size], best first

    def nextValidId(self, orderId: int):
        self.ready.set()

    def updateMktDepthL2(self, reqId, position, marketMaker, operation, side, price, size, isSmartDepth):
        book = self.bids if side == 1 else self.asks  # side: 1 = bid, 0 = ask
        row = [marketMaker, price, size]
        if operation == 0:  # insert: the rows below move down one
            book.insert(min(position, len(book)), row)
        elif operation == 1 and position < len(book):  # update this row
            book[position] = row
        elif operation == 2 and position < len(book):  # delete: the rows below move up
            del book[position]

    def updateMktDepth(self, reqId, position, operation, side, price, size):
        self.updateMktDepthL2(reqId, position, "", operation, side, price, size, False)

    def error(self, reqId, errorTime, errorCode, errorString, advancedOrderRejectJson=""):
        if errorCode == 317:  # TWS reset its book: empty ours, the updates rebuild it
            self.bids.clear()
            self.asks.clear()
        else:
            print(f"[{errorCode}] req {reqId}: {errorString}")

app = DepthApp()
app.connect("127.0.0.1", 7497, 44)
threading.Thread(target=app.run, daemon=True).start()
if not app.ready.wait(5):
    raise SystemExit("No reply: is TWS or IB Gateway running with the API on this port?")

stock = Contract()
stock.symbol, stock.secType, stock.exchange, stock.currency = "ABCD", "STK", "SMART", "USD"

app.reqMktDepth(REQ_ID, stock, ROWS, True, [])  # True = SMART depth
for _ in range(10):
    time.sleep(1)
    bids, asks = list(app.bids), list(app.asks)
    print("bids", bids[:3], "asks", asks[:3])
app.cancelMktDepth(REQ_ID, True)
app.disconnect()
```

ABCD is a placeholder that returns error 200, no security definition: put in a real symbol you have depth for.

Three details matter, two of them from IBKR's error list:

- **317, "Market depth data has been RESET".** IBKR's reference calls handling it "an integral part of processing the incoming data": empty both sides, and the updates that follow rebuild the book. Ignore it and new rows land on top of stale ones.
- **316, "Market depth data has been HALTED".** The stream has stopped and you must subscribe again.
- **Bounds checks.** An update or delete for a row you do not have is skipped: one bad index should cost you a row, not the reader thread.

The callbacks run on the API thread, so copy the lists before you draw or compute from them, as the loop does.

### The same book with ib_async

`ib_async` keeps the book for you on a ticker:

```python
from ib_async import IB, Stock

ib = IB()
ib.connect("127.0.0.1", 7497, clientId=45)
stock = Stock("ABCD", "SMART", "USD")  # put a real symbol here
if ib.qualifyContracts(stock)[0] is None:
    ib.disconnect()
    raise SystemExit("IBKR does not know that symbol.")

ticker = ib.reqMktDepth(stock, numRows=10, isSmartDepth=True)
for _ in range(10):
    ib.sleep(1)
    for level in ticker.domBids[:3]:
        print("bid", level.marketMaker, level.price, level.size)
    for level in ticker.domAsks[:3]:
        print("ask", level.marketMaker, level.price, level.size)
ib.cancelMktDepth(stock, isSmartDepth=True)
ib.disconnect()
```

Note that `isSmartDepth` defaults to False in `ib_async`, so pass it. Also read how a library applies inserts before you trust it. `ib_async` 2.1.0 clears the book on error 317, but it stores rows by position and treats an insert as "insert or overwrite" rather than shifting the rows below. We shift, because IBKR describes 0 as inserting a new row. If you build on either approach, check your book against TWS's BookTrader on a busy stock for a few minutes.

## IBKR API Level 2 limits

Depth is the scarcest data IBKR sells through the API. From IBKR's [specialized market data lines table](https://www.interactivebrokers.com/docs/general/market-data-subscriptions/market-data-lines/specialized-market-data-lines), checked on October 22, 2026:

| Market data lines | Level 2 books at once |
| --- | --- |
| 100 (the default) | 3 |
| 101 to 400 | 3 |
| 401 to 500 | 4 |
| 601 to 700 | 6 |
| 1001 to 1100 | 10 |

So three Quote Booster packs, which take you to 400 lines, still leave you with 3 books. Ask for a fourth symbol and IBKR's [error 309](https://www.interactivebrokers.com/docs/tws-api/doc/error-handling/error-codes) comes back: "Max number (3) of market depth requests has been reached". The same entry adds that API clients "may make multiple market depth requests for the same security", so the cap counts distinct symbols, not requests. IBKR applies the same limit to TWS's own depth windows (BookTrader, Market Depth and the Integrated Stock Window) as to API clients, and we treat the slots as first come, first served: close any depth window in TWS you are not reading before your code asks for its books.

Two gaps to design around:

- **Odd lots are not in the depth feed.** IBKR says so on the reference page, along with a warning that it cannot guarantee every quoted price is shown.
- **Not every venue offers SMART depth.** `reqMktDepthExchanges()` returns each venue and whether it supports SMART depth or only direct depth. Run it once when a book looks thinner than you expect.

*Screenshot: The amber toast when IBKR refuses a fourth book, with IBKR's own reason after the symbol. Demo data.*

## How Scalpel builds its montage from SMART depth

Here is what our code does with all this, read from Scalpel's source:

- **One connection per desk.** Each desk has its own API client id, 10 for the first desk and up from there, and asks for 15 rows a side of SMART depth on a SMART-routed US stock. Orders travel on a different connection, so a burst of depth never queues in front of a sell. Our pillar on [what the TWS API can and cannot do for scalping](https://scalpeltrade.com/blog/tws-api-scalping) explains the one-connection-per-job design.
- **One row per venue, not per price.** The book keeps IBKR's rows in IBKR's order, best first, each with its venue. Inserts shift, deletes close the gap, and an index outside the book is ignored. Error 317 empties both sides.
- **Depth fails alone.** If IBKR refuses the book (a fourth symbol on a default account, no subscription), only the depth stops: the quote, the tape and the chart keep streaming, and the desk shows **Depth unavailable for** the symbol with IBKR's reason, as in the toast above.
- **Background feeds never ask for depth.** When session recording is on, Scalpel records quotes, prints and bars for the scanner's names on extra connections, but never their depth. With 3 slots handed out first come, first served, a background request could take a book from a desk you are trading.
- **Drawn at about 16 frames a second.** Every message is applied to the book, but the screen gets a fresh copy at most every 60 ms. Redrawing per message wastes CPU the order path needs.
- **Grouping happens on screen, not in the book.** The **Tick** stepper merges rows into wider price buckets, bids rounded down and asks up. The stored book stays one row per venue, so the [iceberg detection](https://scalpeltrade.com/guides/iceberg-orders) still sees each venue's rows as IBKR sent them.
- **The same book prices SIM fills.** In SIM, a marketable buy walks the ask rows for a volume-weighted fill, and the thin bid exit warning walks the bid rows to estimate what selling your position would cost.

*Screenshot: The Level 2 panel for ABCD, built from SMART depth: bids left, asks right, one row per venue, the tape on the right. Demo data.*

How to read the result, rather than build it, is in our guide to [reading Level 2](https://scalpeltrade.com/guides/read-level-2) and the pillar [how to read Level 2](https://scalpeltrade.com/blog/how-to-read-level-2). Our section video shows the montage, tape and chart working together:

*Video: [Reading the market | Scalpel guide 3 of 15](https://www.youtube.com/watch?v=xFl6z-L8YW0) (Scalpel Trade, 1:58)*

If you are writing your first script, start with our [IBKR API Python starter](https://scalpeltrade.com/blog/ibkr-api-python) for the connection basics, and see [the TWS API scanner](https://scalpeltrade.com/blog/tws-api-scanner) for finding which symbols deserve your three books. API access itself is switched on as in our [enable TWS API guide](https://scalpeltrade.com/guides/enable-tws-api).

*Trading carries a significant risk of loss. Nothing here is financial advice.*

## Questions

**How do I get Level 2 data from the IBKR API?**

Call reqMktDepth with the contract, the number of rows per side, and isSmartDepth set to True on a SMART-routed stock. Rows arrive as updateMktDepthL2 messages that insert, update or delete a numbered row, and you need a depth subscription such as Nasdaq TotalView on the account.

**What is SMART depth in the IBKR API?**

A market depth request with isSmartDepth set to True. IBKR merges the books of every venue you subscribe to into one stream, labels each row with its exchange, and returns the same data as TWS's BookTrader.

**How many market depth subscriptions does IBKR allow through the API?**

Three distinct symbols at once on accounts with up to 400 market data lines, rising to 4 at 401 to 500 lines and 10 at 1,001 to 1,100. Several requests for the same symbol use one slot, and TWS's own depth windows share the same limit.

**What does IBKR error 317 mean?**

Market depth data has been reset. Empty both sides of your book and let the updates that follow rebuild it, or new rows will mix with stale ones.

**What does error 309 mean in the TWS API?**

You have reached the maximum number of market depth requests, 3 distinct symbols on a default account. Cancel a book you no longer need or close a depth window in TWS.

**Is IBKR market depth data sampled?**

No. IBKR sends depth without sampling or filtering, unlike the Level 1 quote, which is a snapshot every 250 ms for US stocks. Odd lots are not included in the depth feed.
