Skip to main content
The nano-lob crate provides comprehensive feature extraction from order book snapshots for machine learning models. These features capture market microstructure signals used to predict short-term price movements.

LobFeatureExtractor

The core feature extraction engine extracts 44+ features from the order book state.

Initialization

use nano_lob::features::LobFeatureExtractor;

// Default: tick_size=0.25, qty_scale=100.0
let extractor = LobFeatureExtractor::new();

// Custom parameters for different instruments
let extractor = LobFeatureExtractor::with_params(
    0.25,   // tick_size: ES/NQ futures
    100.0,  // qty_scale: normalize quantities
);
Source: nano-lob/src/features.rs:58-74

Feature Structure

pub struct LobFeatures {
    // Price-based features
    pub microprice: f64,           // Volume-weighted mid price
    pub weighted_mid: f64,         // Depth-weighted mid price
    pub spread: f64,               // Bid-ask spread in ticks
    pub mid_price: f64,            // Simple mid price
    pub best_bid: f64,
    pub best_ask: f64,
    
    // Imbalance features
    pub imbalance_l1: f64,         // Level 1 imbalance (-1 to +1)
    pub imbalance_total: f64,      // Total depth imbalance
    
    // Depth features
    pub bid_depth: f64,            // Total bid quantity (normalized)
    pub ask_depth: f64,            // Total ask quantity (normalized)
    pub bid_levels: [f64; 10],     // Bid quantity at each level
    pub ask_levels: [f64; 10],     // Ask quantity at each level
    pub bid_cumulative: [f64; 10], // Cumulative bid depth
    pub ask_cumulative: [f64; 10], // Cumulative ask depth
}
Source: nano-lob/src/features.rs:10-40

Core Features

Microprice

The microprice is a volume-weighted mid price that accounts for the liquidity at the best bid and ask:
microprice = (bid * ask_qty + ask * bid_qty) / (bid_qty + ask_qty)
This provides a more accurate estimate of the “fair” price than the simple mid:
let microprice = extractor.microprice(&book).unwrap();

// Example calculation:
// bid = 5000.00, bid_qty = 100
// ask = 5000.25, ask_qty = 50
// microprice = (5000.00 * 50 + 5000.25 * 100) / 150
//            = (250000 + 500025) / 150
//            = 5000.17
Implementation:
let total_bbo_qty = bid_q + ask_q;
if total_bbo_qty > 0.0 {
    features.microprice = (bid * ask_q + ask * bid_q) / total_bbo_qty;
} else {
    features.microprice = features.mid_price;
}
Source: nano-lob/src/features.rs:96-102, 158-174

Weighted Mid Price

Weights multiple price levels by inverse distance and quantity:
let weighted_mid = extractor.weighted_mid(&book, 5).unwrap();

// Weight = 1 / (level + 1)
// Level 1: weight = 1.0
// Level 2: weight = 0.5
// Level 3: weight = 0.33
// ...
Implementation:
for i in 0..levels {
    let weight = 1.0 / (i as f64 + 1.0);
    
    if let Some(level) = book.bid_level(i) {
        let qty = f64::from(level.quantity.value());
        bid_sum += level.price.as_f64() * qty * weight;
        bid_weight_sum += qty * weight;
    }
    // ... same for ask side
}

weighted_mid = (bid_sum + ask_sum) / (bid_weight_sum + ask_weight_sum)
Source: nano-lob/src/features.rs:176-206

Book Imbalance

Measures the imbalance between bid and ask liquidity:
imbalance = (bid_qty - ask_qty) / (bid_qty + ask_qty)
Values range from -1 (all asks) to +1 (all bids):
// Level 1 imbalance only
let imb_l1 = extractor.book_imbalance(&book, 1);

// Multi-level imbalance (e.g., top 5 levels)
let imb_5 = extractor.book_imbalance(&book, 5);

// Example:
// bid_qty = 100, ask_qty = 50
// imbalance = (100 - 50) / (100 + 50) = 0.333
// Positive imbalance suggests upward price pressure
Implementation:
pub fn book_imbalance(&self, book: &OrderBook, levels: usize) -> f64 {
    let bid_qty = f64::from(book.total_bid_quantity(levels).value());
    let ask_qty = f64::from(book.total_ask_quantity(levels).value());

    let total = bid_qty + ask_qty;
    if total > 0.0 {
        (bid_qty - ask_qty) / total
    } else {
        0.0
    }
}
Source: nano-lob/src/features.rs:104-106, 208-220

Advanced Features

Order Flow Imbalance (OFI)

OFI tracks changes in order flow between consecutive book states:
let prev_book = /* previous book snapshot */;
let curr_book = /* current book snapshot */;

let ofi = extractor.order_flow_imbalance(&prev_book, &curr_book);
OFI Calculation Logic:
  1. Bid side:
    • If bid price improved (higher): +new_bid_qty
    • If bid price worsened (lower): -old_bid_qty
    • If same price: delta_qty
  2. Ask side:
    • If ask price improved (lower): -new_ask_qty
    • If ask price worsened (higher): +old_ask_qty
    • If same price: -delta_qty
Implementation:
pub fn order_flow_imbalance(
    &self,
    prev_book: &OrderBook,
    curr_book: &OrderBook,
) -> f64 {
    let mut ofi = 0.0;

    // Bid side OFI
    if let (Some((prev_bp, prev_bq)), Some((curr_bp, curr_bq))) = 
        (prev_book.best_bid(), curr_book.best_bid()) 
    {
        if curr_bp > prev_bp {
            ofi += f64::from(curr_bq.value());
        } else if curr_bp < prev_bp {
            ofi -= f64::from(prev_bq.value());
        } else {
            ofi += (i64::from(curr_bq.value()) - i64::from(prev_bq.value())) as f64;
        }
    }

    // Ask side OFI (similar logic)
    // ...

    ofi / self.qty_scale
}
Source: nano-lob/src/features.rs:222-261

VPIN (Volume-Synchronized Probability of Informed Trading)

VPIN estimates the probability of informed trading by analyzing volume buckets:
use nano_lob::features::VpinCalculator;

let mut vpin = VpinCalculator::new(
    1000,  // bucket_size: complete bucket after 1000 contracts traded
    50,    // num_buckets: use last 50 buckets for calculation
);

// Add trades as they occur
vpin.add_trade(Quantity::new(10), true);   // buy
vpin.add_trade(Quantity::new(5), false);   // sell

// Calculate VPIN (0 to 1)
let vpin_value = vpin.calculate();

// High VPIN (>0.7) suggests high probability of informed trading
// Low VPIN (<0.3) suggests more random trading
VPIN Formula:
VPIN = Σ|buy_volume - sell_volume| / Σ(buy_volume + sell_volume)
Implementation:
pub fn calculate(&self) -> f64 {
    if self.buckets.is_empty() {
        return 0.0;
    }

    let mut abs_imbalance_sum = 0.0;
    let mut total_volume = 0.0;

    for (buy, sell) in &self.buckets {
        let buy_f = f64::from(*buy);
        let sell_f = f64::from(*sell);
        abs_imbalance_sum += (buy_f - sell_f).abs();
        total_volume += buy_f + sell_f;
    }

    if total_volume > 0.0 {
        abs_imbalance_sum / total_volume
    } else {
        0.0
    }
}
Source: nano-lob/src/features.rs:298-388

Trade Flow Tracking

Track cumulative trade flow over time:
use nano_lob::features::TradeFlowTracker;

let mut tracker = TradeFlowTracker::new();

// Record trades
tracker.record_trade(Quantity::new(100), true, timestamp);  // buy
tracker.record_trade(Quantity::new(50), false, timestamp);  // sell

// Get net flow
let net = tracker.net_flow();  // 100 - 50 = 50

// Get flow imbalance (-1 to 1)
let imbalance = tracker.flow_imbalance();  // (100-50)/(100+50) = 0.333
Source: nano-lob/src/features.rs:390-451

ML Feature Vector

Convert all features to a flat array for ML model input:
let features_array: [f64; 44] = extractor.to_array(&book);

// Feature layout:
// [0]      microprice
// [1]      weighted_mid
// [2]      spread
// [3]      imbalance_l1
// [4-13]   bid_levels (10 levels)
// [14-23]  ask_levels (10 levels)
// [24-33]  bid_cumulative (10 levels)
// [34-43]  ask_cumulative (10 levels)
Implementation:
pub fn to_array(&self, book: &OrderBook) -> [f64; 44] {
    let features = self.extract(book);
    let mut arr = [0.0; 44];

    arr[0] = features.microprice;
    arr[1] = features.weighted_mid;
    arr[2] = features.spread;
    arr[3] = features.imbalance_l1;

    // Bid levels (10 levels)
    for i in 0..FEATURE_LEVELS {
        arr[4 + i] = features.bid_levels[i];
    }

    // Ask levels (10 levels)
    for i in 0..FEATURE_LEVELS {
        arr[14 + i] = features.ask_levels[i];
    }

    // Bid cumulative
    for i in 0..FEATURE_LEVELS {
        arr[24 + i] = features.bid_cumulative[i];
    }

    // Ask cumulative
    for i in 0..FEATURE_LEVELS {
        arr[34 + i] = features.ask_cumulative[i];
    }

    arr
}
Source: nano-lob/src/features.rs:264-295

Usage Example

Real-Time Feature Extraction

use nano_lob::{OrderBook, LobFeatureExtractor};
use nano_feed::parser::MdpParser;

let mut parser = MdpParser::new();
let mut book = OrderBook::new(1);
let extractor = LobFeatureExtractor::new();

// Process market data stream
loop {
    let (message, remaining) = parser.parse(&buffer)?;
    
    if let MdpMessage::BookUpdate(update) = message {
        book.apply_book_update(&update);
        
        // Extract features
        let features = extractor.extract(&book);
        
        println!("Microprice: {:.2}", features.microprice);
        println!("Spread: {:.2} ticks", features.spread);
        println!("Imbalance L1: {:.3}", features.imbalance_l1);
        println!("Total bid depth: {:.0}", features.bid_depth);
        println!("Total ask depth: {:.0}", features.ask_depth);
        
        // Convert to ML input
        let ml_input: [f64; 44] = extractor.to_array(&book);
        // Feed to model...
    }
}

Temporal Features with History

use nano_lob::snapshot::SnapshotRingBuffer;

let mut history = SnapshotRingBuffer::new(100);  // Keep last 100 snapshots
let extractor = LobFeatureExtractor::new();

// After each book update
let snapshot = book.to_snapshot(timestamp);
history.push(snapshot);

// Calculate OFI over last N snapshots
if let (Some(prev), Some(curr)) = (history.get(history.len() - 2), history.latest()) {
    let prev_book = OrderBook::from_snapshot(prev);
    let curr_book = OrderBook::from_snapshot(curr);
    
    let ofi = extractor.order_flow_imbalance(&prev_book, &curr_book);
    println!("OFI: {:.3}", ofi);
}

Multi-Instrument Tracking

use std::collections::HashMap;

let mut books = HashMap::new();
let mut extractors = HashMap::new();

// ES futures: tick_size = 0.25
books.insert("ES", OrderBook::new(1));
extractors.insert("ES", LobFeatureExtractor::with_params(0.25, 100.0));

// NQ futures: tick_size = 0.25  
books.insert("NQ", OrderBook::new(2));
extractors.insert("NQ", LobFeatureExtractor::with_params(0.25, 50.0));

// Extract features for each instrument
for (symbol, book) in &books {
    let extractor = &extractors[symbol];
    let features = extractor.extract(book);
    println!("{} microprice: {:.2}", symbol, features.microprice);
}

Performance Characteristics

Extraction Latency

  • Full feature extraction: ~500-800ns
  • Microprice only: ~50-100ns
  • Book imbalance: ~100-200ns
  • OFI calculation: ~200-400ns
  • VPIN update: ~50-100ns
Benchmark your system:
cd crates/nano-lob
cargo bench --bench features

Memory Footprint

  • LobFeatures: 440 bytes (44 × f64 + overhead)
  • VpinCalculator: ~1KB (depends on num_buckets)
  • TradeFlowTracker: 32 bytes

Feature Interpretation

Microprice vs Mid Price

  • Mid price: Simple average, ignores liquidity
  • Microprice: Weighted by BBO liquidity, better fair value estimate
  • Use microprice for:
    • Order placement decisions
    • Fair value estimation
    • Spread crossing decisions

Imbalance Signals

  • Positive imbalance (>0.3): More bid liquidity → upward pressure
  • Negative imbalance (<-0.3): More ask liquidity → downward pressure
  • Neutral (-0.2 to 0.2): Balanced book

OFI Interpretation

  • Positive OFI: Net aggressive buying → potential price increase
  • Negative OFI: Net aggressive selling → potential price decrease
  • OFI is a leading indicator (predicts next price move)

VPIN Thresholds

  • VPIN < 0.3: Low informed trading, safer to provide liquidity
  • VPIN 0.3-0.7: Moderate informed trading
  • VPIN > 0.7: High informed trading, higher adverse selection risk

Next Steps