How to Process Order Book Data and Build L2/L3 Books in Nautilus Trader

Create an OrderBook instance with BookType::L2_MBP for aggregated market-by-price data or BookType::L3_MBO for individual market-by-order tracking, then feed it OrderBookDelta objects from your exchange adapter to build a real-time view of market depth.

Processing order book data efficiently is critical for high-frequency trading strategies and market-making systems. In the nautechsystems/nautilus_trader repository, the order book implementation provides a unified interface for handling both aggregated L2 (Market-By-Price) and granular L3 (Market-By-Order) data. This guide explains how to process raw feed data and construct order books using the Rust core and Python bindings.

Understanding Order Book Types in Nautilus Trader

Nautilus Trader models market depth through three logical layers, each optimized for different data granularity requirements.

L1 (MBP - Market-By-Price) represents top-of-book only, storing a single best bid and ask price level. This mode clears the ladder on every replacement and is suitable for simple quote tracking.

L2 (MBP - Market-By-Price) aggregates orders at each price level. The BookLadder stores a BTreeMap<BookPrice, BookLevel> where each level holds the total size for that price, regardless of how many individual orders comprise that total.

L3 (MBO - Market-By-Order) maintains every order individually. While using the same BookLadder structure, each BookLevel contains an IndexMap<OrderId, BookOrder> preserving distinct order IDs and their specific sizes.

Core Data Structures for Order Book Processing

The foundation of order book processing resides in crates/model/src/orderbook/book.rs, which defines the OrderBook struct. When instantiating a book, you specify the desired BookType to determine aggregation behavior:

use nautilus_trader::model::{
    orderbook::OrderBook,
    enums::BookType,
    identifiers::InstrumentId,
};

let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");

// Create L2 aggregated book
let l2_book = OrderBook::new(instrument_id, BookType::L2_MBP);

// Create L3 individual order book
let l3_book = OrderBook::new(instrument_id, BookType::L3_MBO);

The atomic unit of change is OrderBookDelta, defined in crates/model/src/data/delta.rs. Each delta carries a single operation—Add, Update, Delete, or Clear—along with the BookOrder to which it applies.

Processing Order Book Deltas

Exchange adapters convert raw JSON or binary messages into OrderBookDelta objects. The OrderBook exposes two primary entry points for applying these changes, both implemented in crates/model/src/orderbook/book.rs:

  • apply_delta(&mut self, delta: &OrderBookDelta) – Validates the instrument ID and forwards to apply_delta_unchecked
  • apply_deltas(&mut self, deltas: &OrderBookDeltas) – Batch processes a snapshot, typically a Clear followed by multiple Add operations

Internally, apply_delta_unchecked dispatches to add, update, or delete methods based on the delta action. These methods forward to the appropriate BookLadder (bids or asks) defined in crates/model/src/orderbook/ladder.rs.

The ladder handles side-specific logic:

  • L2 aggregation: When book_type != BookType::L1_MBP, the add method verifies positive size, creates a BookPrice, and either merges into an existing BookLevel (summing the aggregated size) or creates a new level
  • L3 individual orders: The same code path stores the full BookOrder inside the level's orders map, preserving every order ID
  • Zero-size handling: For L1, an add with size=0 clears the ladder; for L2/L3, orders with zero or negative size are ignored

Building an L2 Order Book Step-by-Step

The following example demonstrates constructing an L2 aggregated book and applying incremental updates:

use nautilus_trader::model::{
    orderbook::OrderBook,
    data::{OrderBookDelta, BookAction, BookOrder},
    enums::{BookType, OrderSide},
    identifiers::InstrumentId,
    types::{Price, Quantity},
};
use nautilus_core::UnixNanos;

// Initialize the book
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);

// Create first delta - add bid at 20,000 with size 0.5
let delta1 = OrderBookDelta::new(
    instrument_id,
    BookAction::Add,
    BookOrder::new(OrderSide::Buy, Price::from("20000.00"), Quantity::from(0.5), 1),
    0,
    1,
    UnixNanos::now(),
    UnixNanos::now(),
);

book.apply_delta(&delta1).unwrap();

// Verify aggregation - add another order at same price
let delta2 = OrderBookDelta::new(
    instrument_id,
    BookAction::Add,
    BookOrder::new(OrderSide::Buy, Price::from("20000.00"), Quantity::from(0.3), 2),
    0,
    2,
    UnixNanos::now(),
    UnixNanos::now(),
);

book.apply_delta(&delta2).unwrap();

// L2 automatically aggregates: total size at 20,000 is now 0.8
assert_eq!(book.best_bid_size().unwrap(), Quantity::from(0.8));

Building an L3 Order Book Step-by-Step

For strategies requiring individual order tracking, use L3 mode:

let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);

// Add two distinct sell orders at the same price level
let delta1 = OrderBookDelta::new(
    instrument_id,
    BookAction::Add,
    BookOrder::new(OrderSide::Sell, Price::from("20100.00"), Quantity::from(0.4), 10),
    0,
    1,
    UnixNanos::now(),
    UnixNanos::now(),
);

let delta2 = OrderBookDelta::new(
    instrument_id,
    BookAction::Add,
    BookOrder::new(OrderSide::Sell, Price::from("20100.00"), Quantity::from(0.2), 11),
    0,
    2,
    UnixNanos::now(),
    UnixNanos::now(),
);

book.apply_delta(&delta1).unwrap();
book.apply_delta(&delta2).unwrap();

// In L3, orders remain separate within the price level
let level = book.asks(None).next().unwrap();
assert_eq!(level.orders.len(), 2);  // Two separate orders tracked

Querying and Analyzing Order Book Data

The OrderBook API in crates/model/src/orderbook/book.rs provides utilities for strategy development:

  • bids_as_map(depth) – Returns a price → size map for the bid side. For L2, this shows aggregated sizes; for L3, sizes are summed per order.
  • group_bids(group_size, depth) – Buckets quantities into price-size buckets useful for heat-map visualizations.
  • get_avg_px_for_quantity(qty, side) – Calculates the average fill price for a given quantity, essential for slippage estimation.
  • pprint(num_levels, group_size) – Pretty-prints the book for debugging, used extensively in the orderbook_imbalance examples.

Integration with Exchange Adapters

Exchange adapters bridge raw feed data to the OrderBook structure. For example, the dYdX adapter in nautilus_trader/adapters/dydx/endpoints/market/orderbook.py fetches HTTP snapshots and decodes them into DYDXWsOrderbookMessageSnapshotContents, which the adapter converts into OrderBookDelta objects.

The adapter pattern ensures that regardless of the exchange's native format, the core OrderBook receives standardized OrderBookDelta objects containing the instrument ID, action type (Add/Update/Delete/Clear), and the BookOrder details.

Summary

  • Initialize the OrderBook with BookType::L2_MBP for aggregated market-by-price data or BookType::L3_MBO for individual market-by-order tracking.
  • Apply OrderBookDelta objects via apply_delta() or batch updates via apply_deltas() to maintain real-time book state.
  • Leverage the BookLadder logic in crates/model/src/orderbook/ladder.rs for automatic aggregation (L2) or individual order preservation (L3).
  • Query the book using utility methods like best_bid_price(), bids_as_map(), and get_avg_px_for_quantity() to drive trading decisions.

Frequently Asked Questions

What is the difference between L2 and L3 order books in Nautilus Trader?

L2 (Market-By-Price) aggregates all orders at the same price level into a single total size, making it suitable for standard market depth analysis and most trading strategies. L3 (Market-By-Order) maintains each order individually with its unique ID, which is essential for order-specific tracking, queue position analysis, or when you need to distinguish between your own orders and public liquidity.

How do I handle order book snapshots from exchanges?

Use the apply_deltas() method to process snapshots efficiently. Exchange adapters typically send a snapshot as an OrderBookDeltas object containing a Clear action followed by multiple Add actions for each price level. This batch approach in crates/model/src/orderbook/book.rs ensures atomic book updates without partial state.

Can I switch between L2 and L3 modes for the same instrument?

No, the BookType is set at initialization and cannot be changed dynamically. The OrderBook struct in crates/model/src/orderbook/book.rs uses the book type to determine aggregation logic in the underlying BookLadder. If you need both views simultaneously, maintain two separate OrderBook instances fed by the same delta stream.

What performance optimizations exist for high-frequency updates?

The BookLadder in crates/model/src/orderbook/ladder.rs uses a BTreeMap for price levels ensuring O(log n) insertion and deletion, while L3 mode uses an IndexMap for O(1) order lookup by ID. The apply_delta_unchecked method bypasses instrument validation for hot paths, and batch processing via apply_deltas reduces function call overhead during initial snapshots.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →