Streaming L2 Market Depth
End-to-end recipe: subscribe to Level-2 market depth for two tickers (AAPL + TSLA), maintain per-ticker bid/ask books from update_mkt_depth_l2 callbacks, and print the book on each update.
What this shows
- Connecting and waiting for
next_valid_id. - Subscribing to L2 with
req_mkt_depthfor multiple contracts. - Building an in-memory book from
update_mkt_depth_l2events (insert/update/deleteops). - Cleanly cancelling subscriptions and disconnecting.
Run it
IB_USERNAME=... IB_PASSWORD=... cargo run --example l2_aapl_tsla
Optional:
IB_HOST=cdc1.ibllc.com 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 — gateway host (default: cdc1.ibllc.com)
//! DURATION_SECS — how long to collect data (default: 15)
use std::collections::HashMap;
use std::env;
use std::time::{Duration, Instant};
use ibx::api::client::{EClient, EClientConfig, Contract};
use ibx::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_code: i64, error_string: &str, _: &str) {
eprintln!(" error req_id={} code={} msg={}", req_id, error_code, 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: f64,
) {
if let Some(book) = self.books.get_mut(&req_id) {
book.apply(position, "", operation, side, price, size);
}
}
fn update_mkt_depth_l2(
&mut self, req_id: i64, position: i32, market_maker: &str,
operation: i32, side: i32, price: f64, size: f64, _is_smart_depth: bool,
) {
if let Some(book) = self.books.get_mut(&req_id) {
book.apply(position, market_maker, operation, side, price, size);
}
}
}
// ── 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_else(|_| "cdc1.ibllc.com".into());
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,
core_id: None,
}).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)
.expect("Failed to subscribe AAPL depth");
println!("Subscribing TSLA (req_id={})", tsla_id);
client.req_mkt_depth(tsla_id, &tsla(), num_rows, is_smart)
.expect("Failed to subscribe TSLA depth");
// 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 {}s...", duration_secs);
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
let _ = client.cancel_mkt_depth(aapl_id);
let _ = client.cancel_mkt_depth(tsla_id);
// 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.");
}