Nautilus Order Book
Nautilus exposes a high-performance Rust-implemented
OrderBookthat maintains per-instrument book state at L1 (BBO), L2 (market-by-price), or L3 (market-by-order). Strategies subscribe viasubscribe_order_book_deltas,subscribe_order_book_depth(snapshots up to 10 levels), orsubscribe_order_book_at_interval(timed full snapshots). The book ships top-of-book accessors (best_bid_price,spread,midpoint) and analysis primitives (get_avg_px_for_quantity,simulate_fills,get_quantity_for_price). A separateOwnOrderBooktracks your working orders, enabling filtered views that subtract self-quoted size from public liquidity. For Cortana MK3 the relevant question is not “can I plug IBKR into the OrderBook” - yes, the IBKR adapter feeds it natively - but “what book level does IB give us, and do we even need it?” Cortana fires off UW options-flow alerts and IBKR top-of-book quotes; deep LOB microstructure is not on the critical path. The OrderBook becomes interesting only if MK3 ever wants to do its own fill simulation against IB depth.
Cody’s question - answered
Will IBKR plug into Nautilus’s OrderBook? Yes - natively, but bounded by
what IB actually publishes. The Nautilus IBKR adapter
(InteractiveBrokersDataClient) ingests IB’s market data through the
ibapi/TWS API and emits the same normalized objects (QuoteTick,
TradeTick, OrderBookDelta, OrderBookDepth10, Bar) that the DataEngine
feeds into the per-instrument OrderBook in Cache. The integration doc lists
“Market Depth: Level 2 order book data (where available)” alongside
quote/trade/bar streams. So the wiring is built - InstrumentProvider →
DataClient → DataEngine → Cache.order_book(instrument_id) - with no
custom code on Cortana’s side.
Depth caveat - and it matters. What ends up in the OrderBook is bounded by
what IB transmits. For Cortana’s universe (SPY equity + SPY 0DTE options) IB
delivers three tiers: (1) top-of-book quote ticks (best bid/ask + sizes)
and last-trade ticks - always available, drives an L1_MBP book; (2) Level
2 market depth via reqMktDepth - available for instruments where IB has
permissioning and the venue exposes it (US equities yes; SPY options depth is
patchy and often not available at all because OPRA disseminates only the NBBO,
and per-exchange depth requires per-exchange paid feeds); (3) no L3
market-by-order - IB’s TWS API does not expose order-ID-keyed book updates
for any US equity/option venue. So practical max is L2 for SPY shares,
realistically L1 for SPY options. That’s a hard ceiling imposed by
IBKR/OPRA, not by Nautilus.
Native vs. custom work. Native: instrument loading, quote/trade/bar
ingestion, OrderBookDepth10 snapshots when IB depth is subscribed,
OrderBook maintenance, best_bid_price/midpoint/spread accessors,
OwnOrderBook (auto-managed by the cache as Cortana submits orders),
filtered views. Custom: nothing for the order-book wiring itself. The only
custom adapter work in scope for Cortana MK3 is UW data (no native
adapter - see concepts/nautilus-integrations.md), not IBKR book data.
Book types
The OrderBook is parameterized by book type, set per-instrument:
L3_MBO(market-by-order). Tracks every individual order at every price level, keyed by order ID. No structural constraint on orders per level. Required for queue-position modeling. Not available from IBKR.L2_MBP(market-by-price). Aggregates orders into one entry per price level. The standard “depth-of-book” representation. Available from IBKR for instruments with permissioned market depth (most US equities, some futures).L1_MBP(top-of-book). Best bid + best ask only - at most one level per side. Populated fromQuoteTick,TradeTick, orBarstreams as well as from explicit L1 deltas. This is the default for IBKR options and the realistic ceiling for SPY 0DTE.
Top-of-book data types (QuoteTick, TradeTick, Bar) “can also maintain
L1_MBP books” per the doc - meaning even if the strategy only subscribes to
quote ticks, the cached OrderBook for that instrument still gets populated
at L1 automatically. No extra subscription needed for BBO + spread + midpoint.
Subscribing to book data
Three subscription methods on Strategy/Actor:
# L3/L2 incremental deltas (highest fidelity, requires venue depth feed)
self.subscribe_order_book_deltas(instrument_id)
# Aggregated depth snapshots, up to 10 levels
self.subscribe_order_book_depth(instrument_id)
# Full book snapshots at a timed interval (lower-rate polling pattern)
self.subscribe_order_book_at_interval(instrument_id, interval_ms=1000)Each delivers to a paired handler:
def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None: ...
def on_order_book_depth(self, depth: OrderBookDepth10) -> None: ...
def on_order_book(self, order_book: OrderBook) -> None: ...The DataEngine’s delta accumulation rule (from concepts/nautilus-concepts.md):
deltas carry flags so “the DataEngine accumulates deltas and only publishes to
subscribers when it encounters F_LAST” - strategies never see partial book
updates. This is a built-in invariant Cortana would otherwise have to
implement by hand.
Accessing the book
// Rust API (the Python PyO3 bindings expose the same shape via
// nautilus_pyo3.OrderBook)
let best_bid: Option<Price> = book.best_bid_price();
let best_ask: Option<Price> = book.best_ask_price();
let spread: Option<f64> = book.spread();
let midpoint: Option<f64> = book.midpoint();In Python, strategies pull the cached book by instrument ID:
book = self.cache.order_book(instrument_id)
best_bid = book.best_bid_price()
mid = book.midpoint()The cache-then-publish invariant from nautilus-concepts.md means: when a
quote tick fires on_quote_tick, the OrderBook in cache is already
updated with that quote - no race window between handler entry and book
visibility.
Analysis methods
The OrderBook exposes a small but useful execution-simulation toolkit
straight from the framework:
// Average fill price for a given quantity
let avg_px = book.get_avg_px_for_quantity(quantity, OrderSide::Buy);
// Average price + qty for a target notional exposure
let (price, qty, exposure) =
book.get_avg_px_qty_for_exposure(target_exposure, OrderSide::Buy);
// Cumulative qty available at or better than a price
let qty = book.get_quantity_for_price(price, OrderSide::Buy);
// Qty at one specific price level
let qty = book.get_quantity_at_level(price, OrderSide::Buy, 2);
// Full simulated fills against the book
let fills: Vec<(Price, Quantity)> = book.simulate_fills(&order);
// All crossed levels regardless of order quantity
let levels = book.get_all_crossed_levels(OrderSide::Buy, price, 2);These are the primitives that make Nautilus’s backtest fill simulation internally consistent with what a market order would do against the same book. The same calls are also valid in live, against the live book.
Integrity checks
book_check_integrity validates state vs. type:
L1_MBP: at most one level per sideL2_MBP: at most one order per price levelL3_MBO: no structural constraints- All types: best bid must not exceed best ask (no crossed book; locked bid==ask is valid)
These run internally during delta application - feed-corruption symptoms get
caught at the framework boundary instead of corrupting downstream pricing.
Instrument-ID mismatches between deltas and the book they target return
BookIntegrityError::InstrumentMismatch.
Pretty printing
For debugging, both OrderBook and OwnOrderBook provide pprint(depth, group_size):
book.pprint(5, None); // 5 levels per side, native ticks
book.pprint(5, Some(Decimal::new(1, 2))); // bucketed at 0.01Useful for fine-tick instruments - group_size buckets levels into coarser groups for visual scanning.
Own order book
Distinct from the public OrderBook. OwnOrderBook tracks your own
working orders separately, so market-making and liquidity-aware strategies
can compute “true available liquidity at each price level (public size minus
your own orders).”
The cache maintains OwnOrderBook automatically - submit, accept, partial
fill, full fill events all flow into it. Each OwnBookOrder carries:
status-SUBMITTED,ACCEPTED,PARTIALLY_FILLED, etc.ts_submitted,ts_accepted- venue acceptance timestamps used by filtering logic
audit_open_orders(known_open_ids) reconciles the OwnOrderBook against the
execution system’s authoritative open-order set. Anything in the book not in
the set is removed and logged as an audit error. The cache calls this
periodically.
Filtered views
Subtract own size from the public book to get net liquidity:
let net_bids = book.bids_filtered_as_map(Some(10), Some(&own_book), None, None, None);
let net_asks = book.asks_filtered_as_map(Some(10), Some(&own_book), None, None, None);
// Full filtered OrderBook with all analysis methods available
let filtered = book.filtered_view(Some(&own_book), Some(10), None, None, None);
let avg_px = filtered.get_avg_px_for_quantity(quantity, OrderSide::Buy);Status + time filtering: only subtract orders matching an OrderStatus set,
optionally with an accepted_buffer_ns grace period (orders accepted within
the last N nanoseconds are excluded - they may not yet appear in the public
feed, so subtracting them double-counts).
let filtered = book.filtered_view(
Some(&own_book),
None,
None,
Some(500_000_000), // 500ms accepted buffer
Some(clock.timestamp_ns()),
);Binary markets (Polymarket-style)
OwnOrderBook::combined_with_opposite handles YES/NO complementary pairs
where price_yes + price_no = 1.0. NO-side asks at P become YES-side bids at
1-P in the combined book and vice versa. Not relevant to Cortana but worth
knowing the OrderBook abstraction covers prediction-market structure.
Backtest vs. live behaviour
The same OrderBook lives in both environments - Nautilus’s
backtest-live-parity story applies here. In backtest the simulated venue
“processes data with strict ordering - exchange updates its book from
incoming data, strategies receive processed data through callbacks, venue
commands settle within the same timestamp” (per nautilus-concepts.md),
which means simulate_fills, get_avg_px_for_quantity, etc., return the
same answers in backtest as the analogous live calls would on the same book
state.
The simulated venue respects L1_MBP/L2_MBP/L3_MBO config so book-type
choice carries from sim to live with no code change. This also means the
ceiling on backtest fidelity equals the ceiling on live data - backtests
on L1 quote-tick data cannot magically simulate L2 fills, and vice versa.
IBKR-specific subsection
What IBKR delivers
- Top-of-book -
QuoteTick(bid + ask + sizes) andTradeTick(last price + size + side). Universal across IB-supported instruments (subject to market-data subscription tier -REALTIME,DELAYED,DELAYED_FROZEN,FROZEN). - Market depth (L2) - via
reqMktDepthunder the hood. Available for instruments where IB has venue-level depth permissioning. US equities generally yes (with subscription); options patchy. - Bars - 1-second through 1-month aggregations.
- No L3 - IB does not expose market-by-order anywhere in its product.
The integration doc enumerates the data client’s surface as: “Quote Ticks / Trade Ticks / Market Depth: Level 2 order book data (where available) / Bar data” - wording matches: Nautilus translates IB’s whatever-is-available into normalized types and feeds them through DataEngine into Cache → OrderBook.
How the IBKR adapter populates the OrderBook
Mechanically (consistent with nautilus-concepts.md’s adapter pattern):
InteractiveBrokersDataClient.connect()opens the TWS/Gateway socket.InteractiveBrokersInstrumentProviderloads contract specs (includingbuild_options_chain=Truefor chain expansion).subscribe_quote_ticks(instrument_id)triggersreqMktDataunder the hood; the adapter parses incoming ticks and emitsQuoteTickobjects. These flow through DataEngine, which writes to Cache before publishing on the bus (cache-then-publish invariant). The instrument’s L1 OrderBook is updated as part of that write.subscribe_order_book_deltas(instrument_id)(if depth is available) triggersreqMktDepth; the adapter emitsOrderBookDeltaevents; DataEngine accumulates untilF_LAST, writes the consolidated update to Cache’s L2 OrderBook, then publishes.subscribe_order_book_at_interval(instrument_id, interval_ms=N)polls the current cached book and re-publishes a full snapshot every N ms - useful for low-frequency consumers that want a stable shape.
Caveats specific to options data (Cortana’s domain)
- OPRA NBBO ceiling. The consolidated US options tape (OPRA) disseminates the National Best Bid/Offer plus per-exchange last trades. Per-exchange depth-of-book is a separate paid feed per options exchange (CBOE C1, ISE, PHLX, NDX, etc.). IB does not bundle these; for retail accounts realistic options data is L1 only. Nautilus will faithfully deliver whatever IB sends, but L1 is the realistic ceiling for SPY options.
- 0DTE chain volatility in book quality. SPY 0DTE strikes near the money
have tight, fast-updating L1 quotes (wide market-maker participation);
far-OTM strikes can have stale or missing quotes. The OrderBook’s
best_bid_price()/best_ask_price()will returnOption<Price>(None when no quote is live) - handlers must check. - Greek-aware fills.
simulate_fillsandget_avg_px_for_quantitydo not model option-specific dynamics (volatility-skew impact, MM quote shading on size). For options fill simulation that respects skew, custom logic is required regardless of book level. - Data-permission cliff.
IBMarketDataTypeEnum.DELAYED_FROZENworks for development without subscriptions but freezes after market close; live trading needsREALTIMEand the corresponding paid feeds. - TWS UTC requirement - gateway must be set to emit UTC timestamps
before Nautilus connects (per
concepts/nautilus-integrations.md). Book timestamps inherit TWS’s wall clock, so this is non-optional.
IB depth in practice
If Cortana ever wanted L2 on SPY shares (not options), the call sequence is:
def on_start(self):
spy_id = InstrumentId.from_str("SPY.ARCA")
self.subscribe_order_book_deltas(spy_id)
def on_order_book_deltas(self, deltas):
book = self.cache.order_book(deltas.instrument_id)
avg_px = book.get_avg_px_for_quantity(qty, OrderSide.BUY)
# ... use book.midpoint() etc.That works against IBKR with the right market-data subscription. For SPY
options there’s no L2 to subscribe to - a subscribe_order_book_deltas call
on a SPY option contract will get the “available” subset (often nothing
beyond NBBO).
Cortana MK3 implications
Do we need the OrderBook at all?
Probably not as a first-class signal source. Cortana’s edge is upstream of LOB microstructure: UW options flow (sweeps, blocks, premium prints, GEX flips) generates the directional signal; IBKR provides the execution venue plus a price-of-record for fill simulation. None of Cortana’s current scoring, gating, or position-management logic reads from a depth book - it reads from quote ticks (best bid/ask), trade prints, and UW WebSocket events.
The OrderBook abstraction is most valuable for strategies that:
- Quote both sides (market making) - Cortana doesn’t.
- Slice large orders by liquidity (TWAP/VWAP/iceberg with depth-aware sizing) - Cortana sizes 1-50 contracts, not relevant.
- Model queue position to estimate fill probability - too fine-grained for 0DTE option entries.
- Read imbalance/microprice as a leading signal - this is the one potentially-relevant case; see “When would we want it” below.
When we would want order book data
-
Microprice / imbalance as a leading signal for SPY shares (not options). Microprice =
(bid_size * ask + ask_size * bid) / (bid_size + ask_size). It leads the midprice when one side has materially more size, and on SPY shares we do have L2 depth available. This is the kind of leading-signal source the “early and right” mandate (project_early_and_right_mandate.md) calls for. It’s a candidate for MK3 V3-flow research, not an MVP feature. Open question: does SPY-shares-microprice lead SPY-options price enough to matter once UW flow is already firing? -
IB price-of-record for fill simulation. Today Cortana uses IB quote ticks as the pricing source of truth (
feedback_ibkr_pricing_source.md). Backtest fills currently use the trade tick or quote midpoint at signal time. Adopting Nautilus’ssimulate_fillsagainst the cachedOrderBook(even at L1) would give a more honest fill model - the bid for a sell, the ask for a buy - without changing the data shape. This is upside on backtest fidelity, not signal quality. -
Spread regime detection.
book.spread()exposes a normalized spread in float - useful as a regime feature (wide spread = uncertain market-maker pricing, often around opens/closes/news). EOD power-hour detection (project_eod_power_hour.md) could ingest this directly without writing a custom spread-tracker.
What we wouldn’t get from IB even if we wanted it
- Queue position. No L3 from IB → no order-ID-keyed book → can’t model “where does my limit sit in the queue at this price?” This is fine; it doesn’t fit Cortana’s strategy class.
- Per-exchange options depth. Only NBBO comes through OPRA; unless we pay for per-exchange options feeds (we won’t), options book stays L1.
- Hidden-order awareness. Iceberg/displayed-only logic requires L3 alongside venue-published metadata; not exposed via IB.
Net for the spike
- The spike’s “is Nautilus a real candidate” question does not hinge on
OrderBook richness. IBKR’s data shape + Nautilus’s normalization is
sufficient for everything Cortana does today (
QuoteTick,TradeTick,Bar). - The OrderBook is nice-to-have framework infrastructure: free L1 book
maintenance, free analysis primitives (
midpoint,spread,simulate_fills), free OwnOrderBook with audit. Cortana would use these passively rather than as a signal source. - The genuinely novel data work for MK3 is UW custom adapter, not
IBKR-side book wiring (per
concepts/nautilus-integrations.md).
See Also
- Nautilus Integrations - IBKR adapter details
(config, ports, paper account
DUP696099mapping, options chain loading, caveats) - Nautilus Concepts - DataEngine, Cache, cache-then-publish invariant, backtest-live parity model
~/.claude/projects/.../memory/feedback_ibkr_pricing_source.md- IBKR is the sole pricing source of truth; UW for scoring/flow only~/.claude/projects/.../memory/project_v1_latency_april16.md- current V1 composite fires AT tops/bottoms; microprice/imbalance is one candidate leading-signal source
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep.