rusty_backtest/
orderbook.rs

1//! L2 Order Book - Simple, accurate, and efficient
2//!
3//! A clean L2 order book implementation optimized for backtesting accuracy.
4//! Maintains price levels with aggregated quantities and order counts.
5
6use parking_lot::RwLock;
7use rust_decimal::Decimal;
8use rust_decimal::prelude::ToPrimitive;
9use rusty_common::SmartString;
10use smallvec::SmallVec;
11use std::collections::BTreeMap;
12use std::sync::Arc;
13
14/// L2 price level with aggregated quantity and order count
15///
16/// Cache-aligned to 64 bytes to prevent false sharing in concurrent access
17#[repr(align(64))]
18#[derive(Debug, Clone)]
19pub struct Level {
20    /// Price at this level
21    pub price: Decimal,
22    /// Total aggregated quantity at this price level
23    pub quantity: Decimal,
24    /// Number of orders at this level (if provided by exchange)
25    pub order_count: u32,
26    /// Padding to reach cache line size
27    _padding: [u8; 28],
28}
29
30impl Default for Level {
31    fn default() -> Self {
32        Self {
33            price: Decimal::ZERO,
34            quantity: Decimal::ZERO,
35            order_count: 0,
36            _padding: [0; 28],
37        }
38    }
39}
40
41impl Level {
42    /// Create a new Level
43    #[must_use]
44    pub const fn new(price: Decimal, quantity: Decimal, order_count: u32) -> Self {
45        Self {
46            price,
47            quantity,
48            order_count,
49            _padding: [0; 28],
50        }
51    }
52}
53
54/// L2 Order Book maintaining bid/ask levels
55pub struct OrderBook {
56    /// Symbol/asset identifier
57    pub symbol: SmartString,
58    /// Tick size for price rounding
59    pub tick_size: Decimal,
60    /// Lot size for quantity rounding
61    pub lot_size: Decimal,
62    /// Bid levels (price -> level) - using BTreeMap for sorted order
63    bids: Arc<RwLock<BTreeMap<i64, Level>>>,
64    /// Ask levels (price -> level) - using BTreeMap for sorted order
65    asks: Arc<RwLock<BTreeMap<i64, Level>>>,
66    /// Last trade price
67    last_price: Arc<RwLock<Option<Decimal>>>,
68    /// Last update timestamp
69    last_update_ns: Arc<RwLock<u64>>,
70}
71
72impl OrderBook {
73    /// Create a new L2 order book
74    #[must_use]
75    pub fn new(symbol: SmartString, tick_size: Decimal, lot_size: Decimal) -> Self {
76        Self {
77            symbol,
78            tick_size,
79            lot_size,
80            bids: Arc::new(RwLock::new(BTreeMap::new())),
81            asks: Arc::new(RwLock::new(BTreeMap::new())),
82            last_price: Arc::new(RwLock::new(None)),
83            last_update_ns: Arc::new(RwLock::new(0)),
84        }
85    }
86
87    /// Convert price to tick
88    #[inline]
89    pub fn price_to_tick(&self, price: Decimal) -> i64 {
90        (price / self.tick_size).round().to_i64().unwrap_or(0)
91    }
92
93    /// Convert tick to price
94    #[inline]
95    pub fn tick_to_price(&self, tick: i64) -> Decimal {
96        Decimal::from(tick) * self.tick_size
97    }
98
99    /// Round quantity to lot size
100    #[inline]
101    pub fn round_quantity(&self, quantity: Decimal) -> Decimal {
102        (quantity / self.lot_size).round() * self.lot_size
103    }
104
105    /// Update a bid level
106    pub fn update_bid(
107        &self,
108        price: Decimal,
109        quantity: Decimal,
110        order_count: u32,
111        timestamp_ns: u64,
112    ) {
113        let tick = self.price_to_tick(price);
114        let rounded_qty = self.round_quantity(quantity);
115
116        let mut bids = self.bids.write();
117        if rounded_qty > Decimal::ZERO && order_count > 0 {
118            bids.insert(tick, Level::new(price, rounded_qty, order_count));
119        } else {
120            bids.remove(&tick);
121        }
122
123        *self.last_update_ns.write() = timestamp_ns;
124    }
125
126    /// Update an ask level
127    pub fn update_ask(
128        &self,
129        price: Decimal,
130        quantity: Decimal,
131        order_count: u32,
132        timestamp_ns: u64,
133    ) {
134        let tick = self.price_to_tick(price);
135        let rounded_qty = self.round_quantity(quantity);
136
137        let mut asks = self.asks.write();
138        if rounded_qty > Decimal::ZERO && order_count > 0 {
139            asks.insert(tick, Level::new(price, rounded_qty, order_count));
140        } else {
141            asks.remove(&tick);
142        }
143
144        *self.last_update_ns.write() = timestamp_ns;
145    }
146
147    /// Get best bid
148    pub fn best_bid(&self) -> Option<Level> {
149        self.bids.read().values().next_back().cloned()
150    }
151
152    /// Get best ask
153    pub fn best_ask(&self) -> Option<Level> {
154        self.asks.read().values().next().cloned()
155    }
156
157    /// Get bid quantity at price tick
158    pub fn bid_qty_at_tick(&self, tick: i64) -> Decimal {
159        self.bids
160            .read()
161            .get(&tick)
162            .map(|l| l.quantity)
163            .unwrap_or(Decimal::ZERO)
164    }
165
166    /// Get ask quantity at price tick
167    pub fn ask_qty_at_tick(&self, tick: i64) -> Decimal {
168        self.asks
169            .read()
170            .get(&tick)
171            .map(|l| l.quantity)
172            .unwrap_or(Decimal::ZERO)
173    }
174
175    /// Get top N bid levels
176    pub fn top_bids(&self, n: usize) -> SmallVec<[Level; 10]> {
177        self.bids.read().values().rev().take(n).cloned().collect()
178    }
179
180    /// Get top N ask levels
181    pub fn top_asks(&self, n: usize) -> SmallVec<[Level; 10]> {
182        self.asks.read().values().take(n).cloned().collect()
183    }
184
185    /// Get mid price
186    pub fn mid_price(&self) -> Option<Decimal> {
187        let bid = self.best_bid()?;
188        let ask = self.best_ask()?;
189        Some((bid.price + ask.price) / Decimal::from(2))
190    }
191
192    /// Get spread
193    pub fn spread(&self) -> Option<Decimal> {
194        let bid = self.best_bid()?;
195        let ask = self.best_ask()?;
196        Some(ask.price - bid.price)
197    }
198
199    /// Update last trade price
200    pub fn update_last_price(&self, price: Decimal, timestamp_ns: u64) {
201        *self.last_price.write() = Some(price);
202        *self.last_update_ns.write() = timestamp_ns;
203    }
204
205    /// Get last trade price
206    pub fn last_price(&self) -> Option<Decimal> {
207        *self.last_price.read()
208    }
209
210    /// Clear all levels
211    pub fn clear(&self) {
212        self.bids.write().clear();
213        self.asks.write().clear();
214    }
215
216    /// Get total bid liquidity within N ticks from best
217    pub fn bid_liquidity_within(&self, ticks: i64) -> Decimal {
218        let bids = self.bids.read();
219        if let Some(best_level) = bids.values().next_back() {
220            let best_tick = self.price_to_tick(best_level.price);
221            let min_tick = best_tick - ticks;
222
223            bids.range(min_tick..=best_tick)
224                .map(|(_, level)| level.quantity)
225                .sum()
226        } else {
227            Decimal::ZERO
228        }
229    }
230
231    /// Get total ask liquidity within N ticks from best
232    pub fn ask_liquidity_within(&self, ticks: i64) -> Decimal {
233        let asks = self.asks.read();
234        if let Some(best_level) = asks.values().next() {
235            let best_tick = self.price_to_tick(best_level.price);
236            let max_tick = best_tick + ticks;
237
238            asks.range(best_tick..=max_tick)
239                .map(|(_, level)| level.quantity)
240                .sum()
241        } else {
242            Decimal::ZERO
243        }
244    }
245
246    /// Calculate weighted average price for a market buy
247    pub fn impact_bid(&self, quantity: Decimal) -> Option<Decimal> {
248        let asks = self.asks.read();
249        let mut remaining = quantity;
250        let mut total_cost = Decimal::ZERO;
251
252        for level in asks.values() {
253            let fill_qty = remaining.min(level.quantity);
254            total_cost += level.price * fill_qty;
255            remaining -= fill_qty;
256
257            if remaining <= Decimal::ZERO {
258                return Some(total_cost / quantity);
259            }
260        }
261
262        None // Not enough liquidity
263    }
264
265    /// Calculate weighted average price for a market sell
266    pub fn impact_ask(&self, quantity: Decimal) -> Option<Decimal> {
267        let bids = self.bids.read();
268        let mut remaining = quantity;
269        let mut total_cost = Decimal::ZERO;
270
271        for level in bids.values().rev() {
272            let fill_qty = remaining.min(level.quantity);
273            total_cost += level.price * fill_qty;
274            remaining -= fill_qty;
275
276            if remaining <= Decimal::ZERO {
277                return Some(total_cost / quantity);
278            }
279        }
280
281        None // Not enough liquidity
282    }
283
284    /// Get order book imbalance
285    pub fn imbalance(&self) -> f64 {
286        let bid_qty = self.bid_liquidity_within(5);
287        let ask_qty = self.ask_liquidity_within(5);
288        let total = bid_qty + ask_qty;
289
290        if total > Decimal::ZERO {
291            ((bid_qty - ask_qty) / total).to_f64().unwrap_or(f64::NAN)
292        } else {
293            0.0
294        }
295    }
296
297    /// Get total volume across all levels
298    pub fn total_volume(&self) -> Decimal {
299        let bids = self.bids.read();
300        let asks = self.asks.read();
301
302        let bid_volume: Decimal = bids.values().map(|level| level.quantity).sum();
303        let ask_volume: Decimal = asks.values().map(|level| level.quantity).sum();
304
305        bid_volume + ask_volume
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_l2_orderbook() {
315        use std::str::FromStr;
316        let book = OrderBook::new(
317            "BTC-USD".into(),
318            Decimal::from_str("0.01").unwrap(),
319            Decimal::from_str("0.001").unwrap(),
320        );
321
322        // Add some levels
323        book.update_bid(
324            Decimal::from(50000),
325            Decimal::from_str("1.5").unwrap(),
326            3,
327            100,
328        );
329        book.update_bid(
330            Decimal::from(49999),
331            Decimal::from_str("2.0").unwrap(),
332            5,
333            101,
334        );
335        book.update_ask(
336            Decimal::from(50001),
337            Decimal::from_str("1.2").unwrap(),
338            2,
339            102,
340        );
341        book.update_ask(
342            Decimal::from(50002),
343            Decimal::from_str("2.5").unwrap(),
344            4,
345            103,
346        );
347
348        // Check best bid/ask
349        assert_eq!(book.best_bid().unwrap().price, Decimal::from(50000));
350        assert_eq!(book.best_ask().unwrap().price, Decimal::from(50001));
351
352        // Check spread
353        assert_eq!(book.spread().unwrap(), Decimal::from(1));
354
355        // Check mid price
356        assert_eq!(
357            book.mid_price().unwrap(),
358            Decimal::from_str("50000.5").unwrap()
359        );
360
361        // Check imbalance
362        let imbalance = book.imbalance();
363        assert!(imbalance > 0.0); // More bids than asks
364    }
365}