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 both books at the end.
What this shows
req_mkt_depth(req_id, &contract, num_rows, is_smart_depth), 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 per level change, carrying the level position, the market
maker, and:
| 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 each book 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. A book that stays empty means the subscription
never started, not that the market is empty.
The example asserts at the end that both books received updates and that neither is empty, so it panics rather than reporting success on a feed that never delivered. Run it while the market is open.
Run it
IB_USERNAME=... IB_PASSWORD=... cargo run --example l2_aapl_tsla
Collect for longer:
DURATION_SECS=30 cargo run --example l2_aapl_tsla
Source
//! Subscribe to L2 market depth for AAPL + TSLA and print the order book.
//!
//! Usage:
//! IB_USERNAME=user IB_PASSWORD=pass cargo run --example l2_aapl_tsla
//!
//! Optional env vars:
//! IB_HOST — the venue host to log in to (default: the venue names it)
//! DURATION_SECS — how long to collect data (default: 15)
use std::collections::HashMap;
use std::env;
use std::time::{Duration, Instant};
use ib_dx::api::client::{EClient, EClientConfig, Contract};
use ib_dx::api::wrapper::Wrapper;
// ── Book entry ──
#[derive(Debug, Clone)]
struct BookLevel {
price: f64,
size: f64,
market_maker: String,
}
// ── Per-ticker book ──
struct TickerBook {
symbol: String,
bids: HashMap<i32, BookLevel>, // position → level
asks: HashMap<i32, BookLevel>,
update_count: u64,
}
impl TickerBook {
fn new(symbol: &str) -> Self {
Self {
symbol: symbol.into(),
bids: HashMap::new(),
asks: HashMap::new(),
update_count: 0,
}
}
fn apply(&mut self, position: i32, market_maker: &str, operation: i32, side: i32, price: f64, size: f64) {
self.update_count += 1;
let book = if side == 1 { &mut self.bids } else { &mut self.asks };
match operation {
0 | 1 => { book.insert(position, BookLevel { price, size, market_maker: market_maker.into() }); }
2 => { book.remove(&position); }
_ => {}
}
}
fn print_summary(&self) {
let mut bids: Vec<_> = self.bids.values().collect();
let mut asks: Vec<_> = self.asks.values().collect();
bids.sort_by(|a, b| b.price.partial_cmp(&a.price).unwrap());
asks.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap());
println!("\n── {} ── ({} updates)", self.symbol, self.update_count);
println!(" Top 5 Asks:");
for level in asks.iter().take(5).rev() {
println!(" {:>10.2} x {:<10.0} {}", level.price, level.size, level.market_maker);
}
println!(" ─────────────────────────────");
println!(" Top 5 Bids:");
for level in bids.iter().take(5) {
println!(" {:>10.2} x {:<10.0} {}", level.price, level.size, level.market_maker);
}
}
}
// ── Wrapper that collects depth for 2 tickers ──
struct DepthWrapper {
books: HashMap<i64, TickerBook>, // req_id → book
errors: Vec<(i64, i64, String)>,
}
impl DepthWrapper {
fn new(tickers: &[(i64, &str)]) -> Self {
let mut books = HashMap::new();
for &(req_id, symbol) in tickers {
books.insert(req_id, TickerBook::new(symbol));
}
Self { books, errors: Vec::new() }
}
fn total_updates(&self) -> u64 {
self.books.values().map(|b| b.update_count).sum()
}
}
impl Wrapper for DepthWrapper {
fn error(&mut self, req_id: i64, _error_time: i64, error_code: i64, error_string: &str, _: &str) {
eprintln!(" error req_id={req_id} code={error_code} msg={error_string}");
self.errors.push((req_id, error_code, error_string.into()));
}
fn update_mkt_depth(
&mut self, req_id: i64, position: i32, operation: i32,
side: i32, price: f64, size: ib_dx::api::Decimal,
) {
if let Some(book) = self.books.get_mut(&req_id) {
book.apply(position, "", operation, side, price, size.into());
}
}
fn update_mkt_depth_l2(
&mut self, req_id: i64, position: i32, market_maker: &str,
operation: i32, side: i32, price: f64, size: ib_dx::api::Decimal, _is_smart_depth: bool,
) {
if let Some(book) = self.books.get_mut(&req_id) {
book.apply(position, market_maker, operation, side, price, size.into());
}
}
}
// ── Contracts ──
fn aapl() -> Contract {
Contract {
con_id: 265598,
symbol: "AAPL".into(),
sec_type: "STK".into(),
exchange: "SMART".into(),
currency: "USD".into(),
..Default::default()
}
}
fn tsla() -> Contract {
Contract {
con_id: 76792991,
symbol: "TSLA".into(),
sec_type: "STK".into(),
exchange: "SMART".into(),
currency: "USD".into(),
..Default::default()
}
}
fn main() {
let username = env::var("IB_USERNAME").expect("IB_USERNAME required");
let password = env::var("IB_PASSWORD").expect("IB_PASSWORD required");
let host = env::var("IB_HOST").unwrap_or_default();
let duration_secs: u64 = env::var("DURATION_SECS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(15);
println!("Connecting to {host}...");
let client = EClient::connect(&EClientConfig {
username,
password,
host,
paper: true,
..Default::default()
}).expect("Failed to connect");
println!("Connected.");
// Subscribe to L2 depth for both tickers (5 rows, SmartDepth)
let num_rows = 5;
let is_smart = true;
let aapl_id: i64 = 1;
let tsla_id: i64 = 2;
println!("Subscribing AAPL (req_id={aapl_id})");
client.req_mkt_depth(aapl_id, &aapl(), num_rows, is_smart);
println!("Subscribing TSLA (req_id={tsla_id})");
client.req_mkt_depth(tsla_id, &tsla(), num_rows, is_smart);
// Poll for updates
let mut wrapper = DepthWrapper::new(&[(aapl_id, "AAPL"), (tsla_id, "TSLA")]);
let start = Instant::now();
let timeout = Duration::from_secs(duration_secs);
println!("Collecting depth data for {duration_secs}s...");
while start.elapsed() < timeout {
client.process_msgs(&mut wrapper);
std::thread::sleep(Duration::from_millis(50));
}
// Print results
for book in wrapper.books.values() {
book.print_summary();
}
let total = wrapper.total_updates();
println!("\nTotal depth updates: {total}");
// Cancel subscriptions
client.cancel_mkt_depth(aapl_id, is_smart);
client.cancel_mkt_depth(tsla_id, is_smart);
// Validate
for (req_id, book) in &wrapper.books {
assert!(
book.update_count > 0,
"No depth updates received for {} (req_id={})", book.symbol, req_id
);
assert!(
!book.bids.is_empty() || !book.asks.is_empty(),
"Empty book for {} (req_id={})", book.symbol, req_id
);
println!("✓ {} — {} updates, {} bid levels, {} ask levels",
book.symbol, book.update_count, book.bids.len(), book.asks.len());
}
assert!(total > 0, "Expected depth updates but got none");
println!("\nAll validations passed.");
}