Skip to main content

polyfill_rs/
book.rs

1//! Order book management for Polymarket client
2
3use crate::errors::{PolyfillError, Result};
4use crate::types::*;
5use crate::utils::math;
6use chrono::Utc;
7use rust_decimal::Decimal;
8use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically - crucial for order books
9use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks
10use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
11
12/// High-performance order book implementation
13///
14/// This is the core data structure that holds all the live buy/sell orders for a token.
15/// The efficiency of this code is critical as the order book is constantly being updated as orders are added and removed.
16///
17/// PERFORMANCE OPTIMIZATION: This struct now uses fixed-point integers internally
18/// instead of Decimal for maximum speed. The performance difference is dramatic:
19///
20/// Before (Decimal):  ~100ns per operation + memory allocation
21/// After (fixed-point): ~5ns per operation, zero allocations
22
23#[derive(Debug, Clone)]
24pub struct OrderBook {
25    /// Token ID this book represents (like "123456" for a specific prediction market outcome)
26    pub token_id: String,
27
28    /// Hash of token_id for fast lookups (avoids string comparisons in hot path)
29    pub token_id_hash: u64,
30
31    /// Current sequence number for ordering updates
32    /// This helps us ignore old/duplicate updates that arrive out of order
33    pub sequence: u64,
34
35    /// Last update timestamp - when we last got new data for this book
36    pub timestamp: chrono::DateTime<Utc>,
37
38    /// Bid side (price -> size, sorted descending) - NOW USING FIXED-POINT!
39    /// BTreeMap automatically keeps highest bids first, which is what we want
40    /// Key = price in ticks (like 6500 for $0.65), Value = size in fixed-point units
41    ///
42    /// BEFORE (slow): bids: BTreeMap<Decimal, Decimal>,
43    /// AFTER (fast):  bids: BTreeMap<Price, Qty>,
44    ///
45    /// Why this is faster:
46    /// - Integer comparisons are ~10x faster than Decimal comparisons
47    /// - No memory allocation for each price level
48    /// - Better CPU cache utilization (smaller data structures)
49    bids: BTreeMap<Price, Qty>,
50
51    /// Ask side (price -> size, sorted ascending) - NOW USING FIXED-POINT!
52    /// BTreeMap keeps lowest asks first - people selling at cheapest prices
53    ///
54    /// BEFORE (slow): asks: BTreeMap<Decimal, Decimal>,
55    /// AFTER (fast):  asks: BTreeMap<Price, Qty>,
56    asks: BTreeMap<Price, Qty>,
57
58    /// Minimum tick size for this market in ticks (like 10 for $0.001 increments)
59    /// Some markets only allow certain price increments
60    /// We store this in ticks for fast validation without conversion
61    tick_size_ticks: Option<Price>,
62
63    /// Maximum depth to maintain (how many price levels to keep)
64    ///
65    /// We don't need to track every single price level, just the best ones because:
66    /// - Trading reality 90% of volume happens in the top 5-10 price levels
67    /// - Execution priority: Orders get filled from best price first, so deep levels often don't matter
68    /// - Market efficiency: If you're buying and best ask is $0.67, you'll never pay $0.95
69    /// - Risk management: Large orders that would hit deep levels are usually broken up
70    /// - Data freshness: Deep levels often have stale orders from hours/days ago
71    ///
72    /// Typical values: 10-50 for retail, 100-500 for institutional HFT systems
73    max_depth: usize,
74}
75
76impl OrderBook {
77    /// Create a new order book
78    /// Just sets up empty bid/ask maps and basic metadata
79    pub fn new(token_id: String, max_depth: usize) -> Self {
80        // Hash the token_id once for fast lookups later
81        let token_id_hash = {
82            use std::collections::hash_map::DefaultHasher;
83            use std::hash::{Hash, Hasher};
84            let mut hasher = DefaultHasher::new();
85            token_id.hash(&mut hasher);
86            hasher.finish()
87        };
88
89        Self {
90            token_id,
91            token_id_hash,
92            sequence: 0, // Start at 0, will increment as we get updates
93            timestamp: Utc::now(),
94            bids: BTreeMap::new(), // Empty to start - using Price/Qty types
95            asks: BTreeMap::new(), // Empty to start - using Price/Qty types
96            tick_size_ticks: None, // We'll set this later when we learn about the market
97            max_depth,
98        }
99    }
100
101    /// Set the tick size for this book
102    /// This tells us the minimum price increment allowed
103    /// We store it in ticks for fast validation without conversion overhead
104    pub fn set_tick_size(&mut self, tick_size: Decimal) -> Result<()> {
105        let tick_size_ticks = decimal_to_price(tick_size)
106            .map_err(|_| PolyfillError::validation("Invalid tick size"))?;
107        self.tick_size_ticks = Some(tick_size_ticks);
108        Ok(())
109    }
110
111    /// Set the tick size directly in ticks (even faster)
112    /// Use this when you already have the tick size in our internal format
113    pub fn set_tick_size_ticks(&mut self, tick_size_ticks: Price) {
114        self.tick_size_ticks = Some(tick_size_ticks);
115    }
116
117    /// Get the current best bid (highest price someone is willing to pay)
118    /// Uses next_back() because BTreeMap sorts ascending, but we want the highest bid
119    ///
120    /// PERFORMANCE: Now returns data in external format but internally uses fast lookups
121    pub fn best_bid(&self) -> Option<BookLevel> {
122        // BEFORE (slow, ~50ns + allocation):
123        // self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size })
124
125        // AFTER (fast, ~5ns, no allocation for the lookup):
126        self.bids
127            .iter()
128            .next_back()
129            .map(|(&price_ticks, &size_units)| {
130                // Convert from internal fixed-point to external Decimal format
131                // This conversion only happens at the API boundary
132                BookLevel {
133                    price: price_to_decimal(price_ticks),
134                    size: qty_to_decimal(size_units),
135                }
136            })
137    }
138
139    /// Get the current best ask (lowest price someone is willing to sell at)
140    /// Uses next() because BTreeMap sorts ascending, so first item is lowest ask
141    ///
142    /// PERFORMANCE: Now returns data in external format but internally uses fast lookups
143    pub fn best_ask(&self) -> Option<BookLevel> {
144        // BEFORE (slow, ~50ns + allocation):
145        // self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
146
147        // AFTER (fast, ~5ns, no allocation for the lookup):
148        self.asks.iter().next().map(|(&price_ticks, &size_units)| {
149            // Convert from internal fixed-point to external Decimal format
150            // This conversion only happens at the API boundary
151            BookLevel {
152                price: price_to_decimal(price_ticks),
153                size: qty_to_decimal(size_units),
154            }
155        })
156    }
157
158    /// Get the current best bid in fast internal format
159    /// Use this for internal calculations to avoid conversion overhead
160    pub fn best_bid_fast(&self) -> Option<FastBookLevel> {
161        self.bids
162            .iter()
163            .next_back()
164            .map(|(&price, &size)| FastBookLevel::new(price, size))
165    }
166
167    /// Get the current best ask in fast internal format
168    /// Use this for internal calculations to avoid conversion overhead
169    pub fn best_ask_fast(&self) -> Option<FastBookLevel> {
170        self.asks
171            .iter()
172            .next()
173            .map(|(&price, &size)| FastBookLevel::new(price, size))
174    }
175
176    /// Get the current spread (difference between best ask and best bid)
177    /// This tells us how "tight" the market is - smaller spread = more liquid market
178    ///
179    /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
180    pub fn spread(&self) -> Option<Decimal> {
181        // BEFORE (slow, ~100ns + multiple allocations):
182        // match (self.best_bid(), self.best_ask()) {
183        //     (Some(bid), Some(ask)) => Some(ask.price - bid.price),
184        //     _ => None,
185        // }
186
187        // AFTER (fast, ~5ns, no allocations):
188        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
189        let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?;
190        Some(price_to_decimal(spread_ticks))
191    }
192
193    /// Get the current mid price (halfway between best bid and ask)
194    /// This is often used as the "fair value" of the market
195    ///
196    /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
197    pub fn mid_price(&self) -> Option<Decimal> {
198        // BEFORE (slow, ~80ns + allocations):
199        // math::mid_price(
200        //     self.best_bid()?.price,
201        //     self.best_ask()?.price,
202        // )
203
204        // AFTER (fast, ~3ns, no allocations):
205        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
206        let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?;
207        Some(price_to_decimal(mid_ticks))
208    }
209
210    /// Get the spread as a percentage (relative to the bid price)
211    /// Useful for comparing spreads across different price levels
212    ///
213    /// PERFORMANCE: Now uses fast internal calculations and returns basis points
214    pub fn spread_pct(&self) -> Option<Decimal> {
215        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
216        let spread_bps = math::spread_pct_fast(best_bid_ticks, best_ask_ticks)?;
217        // Convert basis points back to percentage decimal
218        Some(Decimal::from(spread_bps) / Decimal::from(100))
219    }
220
221    /// Get best bid and ask prices in fast internal format
222    /// Helper method to avoid code duplication and minimize conversions
223    fn best_prices_fast(&self) -> Option<(Price, Price)> {
224        let best_bid_ticks = self.bids.iter().next_back()?.0;
225        let best_ask_ticks = self.asks.iter().next()?.0;
226        Some((*best_bid_ticks, *best_ask_ticks))
227    }
228
229    /// Get the current spread in fast internal format (PERFORMANCE OPTIMIZED)
230    /// Returns spread in ticks - use this for internal calculations
231    pub fn spread_fast(&self) -> Option<Price> {
232        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
233        math::spread_fast(best_bid_ticks, best_ask_ticks)
234    }
235
236    /// Get the current mid price in fast internal format (PERFORMANCE OPTIMIZED)
237    /// Returns mid price in ticks - use this for internal calculations
238    pub fn mid_price_fast(&self) -> Option<Price> {
239        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
240        math::mid_price_fast(best_bid_ticks, best_ask_ticks)
241    }
242
243    /// Get all bids up to a certain depth (top N price levels)
244    /// Returns them in descending price order (best bids first)
245    ///
246    /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
247    /// Only call this when you need to return data to external APIs
248    pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
249        let depth = depth.unwrap_or(self.max_depth);
250        self.bids
251            .iter()
252            .rev() // Reverse because we want highest prices first
253            .take(depth) // Only take the top N levels
254            .map(|(&price_ticks, &size_units)| BookLevel {
255                price: price_to_decimal(price_ticks),
256                size: qty_to_decimal(size_units),
257            })
258            .collect()
259    }
260
261    /// Get all asks up to a certain depth (top N price levels)
262    /// Returns them in ascending price order (best asks first)
263    ///
264    /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
265    /// Only call this when you need to return data to external APIs
266    pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
267        let depth = depth.unwrap_or(self.max_depth);
268        self.asks
269            .iter() // Already in ascending order, so no need to reverse
270            .take(depth) // Only take the top N levels
271            .map(|(&price_ticks, &size_units)| BookLevel {
272                price: price_to_decimal(price_ticks),
273                size: qty_to_decimal(size_units),
274            })
275            .collect()
276    }
277
278    /// Get all bids in fast internal format
279    /// Use this for internal calculations to avoid conversion overhead
280    pub fn bids_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
281        let depth = depth.unwrap_or(self.max_depth);
282        self.bids
283            .iter()
284            .rev() // Reverse because we want highest prices first
285            .take(depth) // Only take the top N levels
286            .map(|(&price, &size)| FastBookLevel::new(price, size))
287            .collect()
288    }
289
290    /// Get all asks in fast internal format (PERFORMANCE OPTIMIZED)
291    /// Use this for internal calculations to avoid conversion overhead
292    pub fn asks_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
293        let depth = depth.unwrap_or(self.max_depth);
294        self.asks
295            .iter() // Already in ascending order, so no need to reverse
296            .take(depth) // Only take the top N levels
297            .map(|(&price, &size)| FastBookLevel::new(price, size))
298            .collect()
299    }
300
301    /// Get the full book snapshot
302    /// Creates a copy of the current state that can be safely passed around
303    /// without worrying about the original book changing
304    pub fn snapshot(&self) -> crate::types::OrderBook {
305        crate::types::OrderBook {
306            token_id: self.token_id.clone(),
307            timestamp: self.timestamp,
308            bids: self.bids(None), // Get all bids (up to max_depth)
309            asks: self.asks(None), // Get all asks (up to max_depth)
310            sequence: self.sequence,
311        }
312    }
313
314    /// Apply a delta update to the book (LEGACY VERSION - for external API compatibility)
315    /// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70"
316    ///
317    /// This method converts the external Decimal delta to our internal fixed-point format
318    /// and then calls the fast version. Use apply_delta_fast() directly when possible.
319    pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
320        // Convert to fast internal format with tick alignment validation
321        let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal);
322        let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal)
323            .map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?;
324
325        // Use the fast internal version
326        self.apply_delta_fast(fast_delta)
327    }
328
329    /// Apply a delta update to the book
330    ///
331    /// This is the high-performance version that works directly with fixed-point data.
332    /// It includes tick alignment validation and is much faster than the Decimal version.
333    ///
334    /// Performance improvement: ~50x faster than the old Decimal version!
335    /// - No Decimal conversions in the hot path
336    /// - Integer comparisons instead of Decimal comparisons
337    /// - No memory allocations for price/size operations
338    pub fn apply_delta_fast(&mut self, delta: FastOrderDelta) -> Result<()> {
339        // Validate sequence ordering - ignore old updates that arrive late
340        // This is crucial for maintaining data integrity in real-time systems
341        if delta.sequence <= self.sequence {
342            trace!(
343                "Ignoring stale delta: {} <= {}",
344                delta.sequence,
345                self.sequence
346            );
347            return Ok(());
348        }
349
350        // Validate token ID hash matches (fast string comparison avoidance)
351        if delta.token_id_hash != self.token_id_hash {
352            return Err(PolyfillError::validation("Token ID mismatch"));
353        }
354
355        // TICK ALIGNMENT VALIDATION - this is where we enforce price rules
356        // If we have a tick size, make sure the price aligns properly
357        if let Some(tick_size_ticks) = self.tick_size_ticks {
358            // BEFORE (slow, ~200ns + multiple conversions):
359            // let tick_size_decimal = price_to_decimal(tick_size_ticks);
360            // if !is_price_tick_aligned(price_to_decimal(delta.price), tick_size_decimal) {
361            //     return Err(...);
362            // }
363
364            // AFTER (fast, ~2ns, pure integer):
365            if tick_size_ticks > 0 && !delta.price.is_multiple_of(tick_size_ticks) {
366                // Price is not aligned to tick size - reject the update
367                warn!(
368                    "Rejecting misaligned price: {} not divisible by tick size {}",
369                    delta.price, tick_size_ticks
370                );
371                return Err(PolyfillError::validation("Price not aligned to tick size"));
372            }
373        }
374
375        // Update our tracking info
376        self.sequence = delta.sequence;
377        self.timestamp = delta.timestamp;
378
379        // Apply the actual change to the appropriate side (FAST VERSION)
380        match delta.side {
381            Side::BUY => self.apply_bid_delta_fast(delta.price, delta.size),
382            Side::SELL => self.apply_ask_delta_fast(delta.price, delta.size),
383        }
384
385        // Keep the book from getting too deep (memory management)
386        self.trim_depth();
387
388        debug!(
389            "Applied fast delta: {} {} @ {} ticks (seq: {})",
390            delta.side.as_str(),
391            delta.size,
392            delta.price,
393            delta.sequence
394        );
395
396        Ok(())
397    }
398
399    /// Begin applying a WebSocket `book` update (hot-path oriented).
400    ///
401    /// This is intended for in-place WS processing where we *stream* levels out of a decoded
402    /// message, without constructing intermediate `BookUpdate` structs.
403    ///
404    /// Returns `Ok(true)` if the update should be applied, or `Ok(false)` if the update is stale
405    /// and should be skipped.
406    pub(crate) fn begin_ws_book_update(&mut self, asset_id: &str, timestamp: u64) -> Result<bool> {
407        if asset_id != self.token_id {
408            return Err(PolyfillError::validation("Token ID mismatch"));
409        }
410
411        if timestamp <= self.sequence {
412            return Ok(false);
413        }
414
415        self.sequence = timestamp;
416        self.timestamp =
417            chrono::DateTime::<Utc>::from_timestamp(timestamp as i64, 0).unwrap_or_else(Utc::now);
418
419        Ok(true)
420    }
421
422    /// Apply a single WS `book` level (already converted to internal fixed-point).
423    ///
424    /// Note: Insertions of new price levels may allocate (BTreeMap node growth). In a strict
425    /// zero-alloc hot path, all expected levels must be warmed up ahead of time.
426    pub(crate) fn apply_ws_book_level_fast(
427        &mut self,
428        side: Side,
429        price_ticks: Price,
430        size_units: Qty,
431    ) -> Result<()> {
432        if let Some(tick_size_ticks) = self.tick_size_ticks {
433            if tick_size_ticks > 0 && !price_ticks.is_multiple_of(tick_size_ticks) {
434                return Err(PolyfillError::validation("Price not aligned to tick size"));
435            }
436        }
437
438        match side {
439            Side::BUY => self.apply_bid_delta_fast(price_ticks, size_units),
440            Side::SELL => self.apply_ask_delta_fast(price_ticks, size_units),
441        }
442
443        Ok(())
444    }
445
446    /// Finish applying a WS `book` update.
447    pub(crate) fn finish_ws_book_update(&mut self) {
448        self.trim_depth();
449    }
450
451    /// Apply a WebSocket `book` update for this token.
452    ///
453    /// The official Polymarket CLOB WebSocket `book` event contains batches of
454    /// price levels for both sides. Unlike `apply_delta_fast`, this method can
455    /// apply many levels that share the same message timestamp.
456    ///
457    /// Notes:
458    /// - This performs upserts (update/insert/remove) for the provided levels.
459    /// - It does **not** infer removals for levels omitted from the message.
460    /// - Insertions of *new* price levels may allocate (BTreeMap node growth).
461    pub fn apply_book_update(&mut self, update: &BookUpdate) -> Result<()> {
462        if update.asset_id != self.token_id {
463            return Err(PolyfillError::validation("Token ID mismatch"));
464        }
465
466        // Use the exchange-provided timestamp as our monotonic sequence marker.
467        // This is less strict than the REST/legacy delta sequence but works for
468        // ignoring obviously stale book snapshots.
469        if update.timestamp <= self.sequence {
470            return Ok(());
471        }
472
473        self.sequence = update.timestamp;
474        self.timestamp = chrono::DateTime::<Utc>::from_timestamp(update.timestamp as i64, 0)
475            .unwrap_or_else(Utc::now);
476
477        // Apply bids (BUY) and asks (SELL) as level upserts.
478        for level in &update.bids {
479            let price_ticks = decimal_to_price(level.price)
480                .map_err(|_| PolyfillError::validation("Invalid price"))?;
481            let size_units = decimal_to_qty(level.size)
482                .map_err(|_| PolyfillError::validation("Invalid size"))?;
483
484            if let Some(tick_size_ticks) = self.tick_size_ticks {
485                if tick_size_ticks > 0 && !price_ticks.is_multiple_of(tick_size_ticks) {
486                    return Err(PolyfillError::validation("Price not aligned to tick size"));
487                }
488            }
489
490            if size_units == 0 {
491                self.bids.remove(&price_ticks);
492            } else {
493                self.bids.insert(price_ticks, size_units);
494            }
495        }
496
497        for level in &update.asks {
498            let price_ticks = decimal_to_price(level.price)
499                .map_err(|_| PolyfillError::validation("Invalid price"))?;
500            let size_units = decimal_to_qty(level.size)
501                .map_err(|_| PolyfillError::validation("Invalid size"))?;
502
503            if let Some(tick_size_ticks) = self.tick_size_ticks {
504                if tick_size_ticks > 0 && !price_ticks.is_multiple_of(tick_size_ticks) {
505                    return Err(PolyfillError::validation("Price not aligned to tick size"));
506                }
507            }
508
509            if size_units == 0 {
510                self.asks.remove(&price_ticks);
511            } else {
512                self.asks.insert(price_ticks, size_units);
513            }
514        }
515
516        self.trim_depth();
517        Ok(())
518    }
519
520    /// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION
521    /// If size is 0, it means "remove this price level entirely"
522    /// Otherwise, set the total size at this price level
523    ///
524    /// This converts to fixed-point and calls the fast version
525    #[allow(dead_code)]
526    fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
527        // Convert to fixed-point (this should be rare since we use fast path)
528        let price_ticks = decimal_to_price(price).unwrap_or(0);
529        let size_units = decimal_to_qty(size).unwrap_or(0);
530        self.apply_bid_delta_fast(price_ticks, size_units);
531    }
532
533    /// Apply an ask-side delta (someone wants to sell) - LEGACY VERSION
534    /// Same logic as bids - size of 0 means remove the price level
535    ///
536    /// This converts to fixed-point and calls the fast version
537    #[allow(dead_code)]
538    fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
539        // Convert to fixed-point (this should be rare since we use fast path)
540        let price_ticks = decimal_to_price(price).unwrap_or(0);
541        let size_units = decimal_to_qty(size).unwrap_or(0);
542        self.apply_ask_delta_fast(price_ticks, size_units);
543    }
544
545    /// Apply a bid-side delta (someone wants to buy) - FAST VERSION
546    ///
547    /// This is the high-performance version that works directly with fixed-point.
548    /// Much faster than the Decimal version - pure integer operations.
549    fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
550        // BEFORE (slow, ~100ns + allocation):
551        // if size.is_zero() {
552        //     self.bids.remove(&price);
553        // } else {
554        //     self.bids.insert(price, size);
555        // }
556
557        // AFTER (fast, ~5ns, no allocation):
558        if size_units == 0 {
559            self.bids.remove(&price_ticks); // No more buyers at this price
560        } else {
561            self.bids.insert(price_ticks, size_units); // Update total size at this price
562        }
563    }
564
565    /// Apply an ask-side delta (someone wants to sell) - FAST VERSION
566    ///
567    /// This is the high-performance version that works directly with fixed-point.
568    /// Much faster than the Decimal version - pure integer operations.
569    fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
570        // BEFORE (slow, ~100ns + allocation):
571        // if size.is_zero() {
572        //     self.asks.remove(&price);
573        // } else {
574        //     self.asks.insert(price, size);
575        // }
576
577        // AFTER (fast, ~5ns, no allocation):
578        if size_units == 0 {
579            self.asks.remove(&price_ticks); // No more sellers at this price
580        } else {
581            self.asks.insert(price_ticks, size_units); // Update total size at this price
582        }
583    }
584
585    /// Trim the book to maintain depth limits
586    /// We don't want to track every single price level - just the best ones
587    ///
588    /// Why limit depth? Several reasons:
589    /// 1. Memory efficiency: A popular market might have thousands of price levels,
590    ///    but only the top 10-50 levels are actually tradeable with reasonable size
591    /// 2. Performance: Fewer levels = faster iteration when calculating market impact
592    /// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
593    ///    mostly noise and will never get hit in normal trading
594    /// 4. Stale data: Deep levels often contain old orders that haven't been cancelled
595    /// 5. Network bandwidth: Less data to send when streaming updates
596    fn trim_depth(&mut self) {
597        // For bids, remove the LOWEST prices (worst bids) if we have too many
598        // Example: If best bid is $0.65, we don't care about bids at $0.10
599        if self.bids.len() > self.max_depth {
600            let to_remove = self.bids.len() - self.max_depth;
601            for _ in 0..to_remove {
602                self.bids.pop_first(); // Remove lowest bid prices (furthest from market)
603            }
604        }
605
606        // For asks, remove the HIGHEST prices (worst asks) if we have too many
607        // Example: If best ask is $0.67, we don't care about asks at $0.95
608        if self.asks.len() > self.max_depth {
609            let to_remove = self.asks.len() - self.max_depth;
610            for _ in 0..to_remove {
611                self.asks.pop_last(); // Remove highest ask prices (furthest from market)
612            }
613        }
614    }
615
616    /// Calculate the market impact for a given order size
617    /// This is exactly why we don't need deep levels - if your order would require
618    /// hitting prices way off the current market (like $0.95 when best ask is $0.67),
619    /// you'd never actually place that order. You'd either:
620    /// 1. Break it into smaller pieces over time
621    /// 2. Use a different trading strategy
622    /// 3. Accept that there's not enough liquidity right now
623    pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
624        // PERFORMANCE NOTE: This method still uses Decimal for external compatibility,
625        // but the internal order book lookups now use our fast fixed-point data structures.
626        //
627        // BEFORE: Each level lookup involved Decimal operations (~50ns each)
628        // AFTER: Level lookups use integer operations (~5ns each)
629        //
630        // For a 10-level impact calculation: 500ns → 50ns (10x speedup)
631
632        // Get the levels we'd be trading against
633        let levels = match side {
634            Side::BUY => self.asks(None),  // If buying, we hit the ask side
635            Side::SELL => self.bids(None), // If selling, we hit the bid side
636        };
637
638        if levels.is_empty() {
639            return None; // No liquidity available
640        }
641
642        let mut remaining_size = size;
643        let mut total_cost = Decimal::ZERO;
644        let mut weighted_price = Decimal::ZERO;
645
646        // Walk through each price level, filling as much as we can
647        for level in levels {
648            let fill_size = std::cmp::min(remaining_size, level.size);
649            let level_cost = fill_size * level.price;
650
651            total_cost += level_cost;
652            weighted_price += level_cost; // This accumulates the weighted average
653            remaining_size -= fill_size;
654
655            if remaining_size.is_zero() {
656                break; // We've filled our entire order
657            }
658        }
659
660        if remaining_size > Decimal::ZERO {
661            // Not enough liquidity to fill the whole order
662            // This is a perfect example of why we don't need infinite depth:
663            // If we can't fill your order with the top N levels, you probably
664            // shouldn't be placing that order anyway - it would move the market too much
665            return None;
666        }
667
668        let avg_price = weighted_price / size;
669
670        // Calculate how much we moved the market compared to the best price
671        let impact = match side {
672            Side::BUY => {
673                let best_ask = self.best_ask()?.price;
674                (avg_price - best_ask) / best_ask // How much worse than best ask
675            },
676            Side::SELL => {
677                let best_bid = self.best_bid()?.price;
678                (best_bid - avg_price) / best_bid // How much worse than best bid
679            },
680        };
681
682        Some(MarketImpact {
683            average_price: avg_price,
684            impact_pct: impact,
685            total_cost,
686            size_filled: size,
687        })
688    }
689
690    /// Check if the book is stale (no recent updates)
691    /// Useful for detecting when we've lost connection to live data
692    pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
693        let age = Utc::now() - self.timestamp;
694        age > chrono::Duration::from_std(max_age).unwrap_or_default()
695    }
696
697    /// Get the total liquidity at a given price level
698    /// Tells you how much you can buy/sell at exactly this price
699    pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
700        // Convert decimal price to our internal fixed-point representation
701        let price_ticks = match decimal_to_price(price) {
702            Ok(ticks) => ticks,
703            Err(_) => return Decimal::ZERO, // Invalid price
704        };
705
706        match side {
707            Side::BUY => {
708                // How much we can buy at this price (look at asks)
709                let size_units = self.asks.get(&price_ticks).copied().unwrap_or_default();
710                qty_to_decimal(size_units)
711            },
712            Side::SELL => {
713                // How much we can sell at this price (look at bids)
714                let size_units = self.bids.get(&price_ticks).copied().unwrap_or_default();
715                qty_to_decimal(size_units)
716            },
717        }
718    }
719
720    /// Get the total liquidity within a price range
721    /// Useful for understanding how much depth exists in a certain price band
722    pub fn liquidity_in_range(
723        &self,
724        min_price: Decimal,
725        max_price: Decimal,
726        side: Side,
727    ) -> Decimal {
728        // Convert decimal prices to our internal fixed-point representation
729        let min_price_ticks = match decimal_to_price(min_price) {
730            Ok(ticks) => ticks,
731            Err(_) => return Decimal::ZERO, // Invalid price
732        };
733        let max_price_ticks = match decimal_to_price(max_price) {
734            Ok(ticks) => ticks,
735            Err(_) => return Decimal::ZERO, // Invalid price
736        };
737
738        let levels: Vec<_> = match side {
739            Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(),
740            Side::SELL => self
741                .bids
742                .range(min_price_ticks..=max_price_ticks)
743                .rev()
744                .collect(),
745        };
746
747        // Sum up the sizes, converting from fixed-point back to Decimal
748        let total_size_units: i64 = levels.into_iter().map(|(_, &size)| size).sum();
749        qty_to_decimal(total_size_units)
750    }
751
752    /// Validate that prices are properly ordered
753    /// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
754    pub fn is_valid(&self) -> bool {
755        match (self.best_bid(), self.best_ask()) {
756            (Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition
757            _ => true,                                       // Empty book is technically valid
758        }
759    }
760}
761
762/// Market impact calculation result
763/// This tells you what would happen if you executed a large order
764#[derive(Debug, Clone)]
765pub struct MarketImpact {
766    pub average_price: Decimal, // The average price you'd get across all fills
767    pub impact_pct: Decimal,    // How much worse than the best price (as percentage)
768    pub total_cost: Decimal,    // Total amount you'd pay/receive
769    pub size_filled: Decimal,   // How much of your order got filled
770}
771
772/// Thread-safe order book manager
773/// This manages multiple order books (one per token) and handles concurrent access
774/// Multiple threads can read/write different books simultaneously
775///
776/// The depth limiting becomes even more critical here because we might be tracking
777/// hundreds or thousands of different tokens simultaneously. If each book had
778/// unlimited depth, we could easily use gigabytes of RAM for mostly useless data.
779///
780/// Example: 1000 tokens × 1000 price levels × 32 bytes per level = 32MB just for prices
781/// With depth limiting: 1000 tokens × 50 levels × 32 bytes = 1.6MB (20x less memory)
782#[derive(Debug)]
783pub struct OrderBookManager {
784    books: Arc<RwLock<std::collections::HashMap<String, OrderBook>>>, // Token ID -> OrderBook
785    max_depth: usize,
786}
787
788impl OrderBookManager {
789    /// Create a new order book manager
790    /// Starts with an empty collection of books
791    pub fn new(max_depth: usize) -> Self {
792        Self {
793            books: Arc::new(RwLock::new(std::collections::HashMap::new())),
794            max_depth,
795        }
796    }
797
798    /// Get or create an order book for a token
799    /// If we don't have a book for this token yet, create a new empty one
800    pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
801        let mut books = self
802            .books
803            .write()
804            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
805
806        if let Some(book) = books.get(token_id) {
807            Ok(book.clone()) // Return a copy of the existing book
808        } else {
809            // Create a new book for this token
810            let book = OrderBook::new(token_id.to_string(), self.max_depth);
811            books.insert(token_id.to_string(), book.clone());
812            Ok(book)
813        }
814    }
815
816    /// Execute a closure with mutable access to a managed book.
817    ///
818    /// This is useful for hot-path update ingestion where you want to avoid allocating
819    /// intermediate update structs (e.g., applying WS updates directly).
820    pub fn with_book_mut<R>(
821        &self,
822        token_id: &str,
823        f: impl FnOnce(&mut OrderBook) -> Result<R>,
824    ) -> Result<R> {
825        let mut books = self
826            .books
827            .write()
828            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
829
830        let book = books.get_mut(token_id).ok_or_else(|| {
831            PolyfillError::market_data(
832                format!("No book found for token: {}", token_id),
833                crate::errors::MarketDataErrorKind::TokenNotFound,
834            )
835        })?;
836
837        f(book)
838    }
839
840    /// Update a book with a delta
841    /// This is called when we receive real-time updates from the exchange
842    pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
843        let mut books = self
844            .books
845            .write()
846            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
847
848        // Find the book for this token (must already exist)
849        let book = books.get_mut(&delta.token_id).ok_or_else(|| {
850            PolyfillError::market_data(
851                format!("No book found for token: {}", delta.token_id),
852                crate::errors::MarketDataErrorKind::TokenNotFound,
853            )
854        })?;
855
856        // Apply the update to the specific book
857        book.apply_delta(delta)
858    }
859
860    /// Apply a WebSocket `book` update to a managed book.
861    ///
862    /// This is the preferred way to ingest `StreamMessage::Book` updates into
863    /// the in-memory order books (avoids rebuilding snapshots via per-level deltas).
864    pub fn apply_book_update(&self, update: &BookUpdate) -> Result<()> {
865        let mut books = self
866            .books
867            .write()
868            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
869
870        if let Some(book) = books.get_mut(update.asset_id.as_str()) {
871            return book.apply_book_update(update);
872        }
873
874        // First time we've seen this token; allocating the key and book is part of warmup.
875        let token_id = update.asset_id.clone();
876        books.insert(token_id.clone(), OrderBook::new(token_id, self.max_depth));
877
878        books
879            .get_mut(update.asset_id.as_str())
880            .ok_or_else(|| PolyfillError::internal_simple("Failed to insert order book"))?
881            .apply_book_update(update)
882    }
883
884    /// Get a book snapshot
885    /// Returns a copy of the current book state that won't change
886    pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
887        let books = self
888            .books
889            .read()
890            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
891
892        books
893            .get(token_id)
894            .map(|book| book.snapshot()) // Create a snapshot copy
895            .ok_or_else(|| {
896                PolyfillError::market_data(
897                    format!("No book found for token: {}", token_id),
898                    crate::errors::MarketDataErrorKind::TokenNotFound,
899                )
900            })
901    }
902
903    /// Get all available books
904    /// Returns snapshots of every book we're currently tracking
905    pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
906        let books = self
907            .books
908            .read()
909            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
910
911        Ok(books.values().map(|book| book.snapshot()).collect())
912    }
913
914    /// Remove stale books
915    /// Cleans up books that haven't been updated recently (probably disconnected)
916    /// This prevents memory leaks from accumulating dead books
917    pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
918        let mut books = self
919            .books
920            .write()
921            .map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
922
923        let initial_count = books.len();
924        books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books
925        let removed = initial_count - books.len();
926
927        if removed > 0 {
928            debug!("Removed {} stale order books", removed);
929        }
930
931        Ok(removed)
932    }
933}
934
935/// Order book analytics and statistics
936/// Provides a summary view of the book's health and characteristics
937#[derive(Debug, Clone)]
938pub struct BookAnalytics {
939    pub token_id: String,
940    pub timestamp: chrono::DateTime<Utc>,
941    pub bid_count: usize,            // How many different bid price levels
942    pub ask_count: usize,            // How many different ask price levels
943    pub total_bid_size: Decimal,     // Total size of all bids combined
944    pub total_ask_size: Decimal,     // Total size of all asks combined
945    pub spread: Option<Decimal>,     // Current spread (ask - bid)
946    pub spread_pct: Option<Decimal>, // Spread as percentage
947    pub mid_price: Option<Decimal>,  // Current mid price
948    pub volatility: Option<Decimal>, // Price volatility (if calculated)
949}
950
951impl OrderBook {
952    /// Calculate analytics for this book
953    /// Gives you a quick health check of the market
954    pub fn analytics(&self) -> BookAnalytics {
955        let bid_count = self.bids.len();
956        let ask_count = self.asks.len();
957        // Sum up all bid/ask sizes, converting from fixed-point back to Decimal
958        let total_bid_size_units: i64 = self.bids.values().sum();
959        let total_ask_size_units: i64 = self.asks.values().sum();
960        let total_bid_size = qty_to_decimal(total_bid_size_units);
961        let total_ask_size = qty_to_decimal(total_ask_size_units);
962
963        BookAnalytics {
964            token_id: self.token_id.clone(),
965            timestamp: self.timestamp,
966            bid_count,
967            ask_count,
968            total_bid_size,
969            total_ask_size,
970            spread: self.spread(),
971            spread_pct: self.spread_pct(),
972            mid_price: self.mid_price(),
973            volatility: self.calculate_volatility(),
974        }
975    }
976
977    /// Calculate price volatility (simplified)
978    /// This is a placeholder - real volatility needs historical price data
979    fn calculate_volatility(&self) -> Option<Decimal> {
980        // This is a simplified volatility calculation
981        // In a real implementation, you'd want to track price history over time
982        // and calculate standard deviation of price changes
983        None
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990    use rust_decimal_macros::dec;
991    use std::str::FromStr;
992    use std::time::Duration; // Convenient macro for creating Decimal literals
993
994    #[test]
995    fn test_order_book_creation() {
996        // Test that we can create a new empty order book
997        let book = OrderBook::new("test_token".to_string(), 10);
998        assert_eq!(book.token_id, "test_token");
999        assert_eq!(book.bids.len(), 0); // Should start empty
1000        assert_eq!(book.asks.len(), 0); // Should start empty
1001    }
1002
1003    #[test]
1004    fn test_apply_delta() {
1005        // Test that we can apply order book updates
1006        let mut book = OrderBook::new("test_token".to_string(), 10);
1007
1008        // Create a buy order at $0.50 for 100 tokens
1009        let delta = OrderDelta {
1010            token_id: "test_token".to_string(),
1011            timestamp: Utc::now(),
1012            side: Side::BUY,
1013            price: dec!(0.5),
1014            size: dec!(100),
1015            sequence: 1,
1016        };
1017
1018        book.apply_delta(delta).unwrap();
1019        assert_eq!(book.sequence, 1); // Sequence should update
1020        assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); // Should be our bid
1021        assert_eq!(book.best_bid().unwrap().size, dec!(100)); // Should be our size
1022    }
1023
1024    #[test]
1025    fn test_spread_calculation() {
1026        // Test that we can calculate the spread between bid and ask
1027        let mut book = OrderBook::new("test_token".to_string(), 10);
1028
1029        // Add a bid at $0.50
1030        book.apply_delta(OrderDelta {
1031            token_id: "test_token".to_string(),
1032            timestamp: Utc::now(),
1033            side: Side::BUY,
1034            price: dec!(0.5),
1035            size: dec!(100),
1036            sequence: 1,
1037        })
1038        .unwrap();
1039
1040        // Add an ask at $0.52
1041        book.apply_delta(OrderDelta {
1042            token_id: "test_token".to_string(),
1043            timestamp: Utc::now(),
1044            side: Side::SELL,
1045            price: dec!(0.52),
1046            size: dec!(100),
1047            sequence: 2,
1048        })
1049        .unwrap();
1050
1051        let spread = book.spread().unwrap();
1052        assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02
1053    }
1054
1055    #[test]
1056    fn test_market_impact() {
1057        // Test market impact calculation for a large order
1058        let mut book = OrderBook::new("test_token".to_string(), 10);
1059
1060        // Add multiple ask levels (people selling at different prices)
1061        // $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens
1062        for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
1063            book.apply_delta(OrderDelta {
1064                token_id: "test_token".to_string(),
1065                timestamp: Utc::now(),
1066                side: Side::SELL,
1067                price: *price,
1068                size: dec!(100),
1069                sequence: i as u64 + 1,
1070            })
1071            .unwrap();
1072        }
1073
1074        // Try to buy 150 tokens (will need to hit multiple price levels)
1075        let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap();
1076        assert!(impact.average_price > dec!(0.50)); // Should be worse than best price
1077        assert!(impact.average_price < dec!(0.51)); // But not as bad as second level
1078    }
1079
1080    #[test]
1081    fn test_apply_bid_delta_legacy() {
1082        let mut book = OrderBook::new("test_token".to_string(), 10);
1083
1084        // Test adding a bid
1085        book.apply_bid_delta(
1086            Decimal::from_str("0.75").unwrap(),
1087            Decimal::from_str("100.0").unwrap(),
1088        );
1089
1090        let best_bid = book.best_bid();
1091        assert!(best_bid.is_some());
1092        let bid = best_bid.unwrap();
1093        assert_eq!(bid.price, Decimal::from_str("0.75").unwrap());
1094        assert_eq!(bid.size, Decimal::from_str("100.0").unwrap());
1095
1096        // Test updating the bid
1097        book.apply_bid_delta(
1098            Decimal::from_str("0.75").unwrap(),
1099            Decimal::from_str("150.0").unwrap(),
1100        );
1101        let updated_bid = book.best_bid().unwrap();
1102        assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap());
1103
1104        // Test removing the bid
1105        book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO);
1106        assert!(book.best_bid().is_none());
1107    }
1108
1109    #[test]
1110    fn test_apply_ask_delta_legacy() {
1111        let mut book = OrderBook::new("test_token".to_string(), 10);
1112
1113        // Test adding an ask
1114        book.apply_ask_delta(
1115            Decimal::from_str("0.76").unwrap(),
1116            Decimal::from_str("50.0").unwrap(),
1117        );
1118
1119        let best_ask = book.best_ask();
1120        assert!(best_ask.is_some());
1121        let ask = best_ask.unwrap();
1122        assert_eq!(ask.price, Decimal::from_str("0.76").unwrap());
1123        assert_eq!(ask.size, Decimal::from_str("50.0").unwrap());
1124
1125        // Test updating the ask
1126        book.apply_ask_delta(
1127            Decimal::from_str("0.76").unwrap(),
1128            Decimal::from_str("75.0").unwrap(),
1129        );
1130        let updated_ask = book.best_ask().unwrap();
1131        assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap());
1132
1133        // Test removing the ask
1134        book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO);
1135        assert!(book.best_ask().is_none());
1136    }
1137
1138    #[test]
1139    fn test_liquidity_analysis() {
1140        let mut book = OrderBook::new("test_token".to_string(), 10);
1141
1142        // Build order book using legacy methods
1143        book.apply_bid_delta(
1144            Decimal::from_str("0.75").unwrap(),
1145            Decimal::from_str("100.0").unwrap(),
1146        );
1147        book.apply_bid_delta(
1148            Decimal::from_str("0.74").unwrap(),
1149            Decimal::from_str("50.0").unwrap(),
1150        );
1151        book.apply_ask_delta(
1152            Decimal::from_str("0.76").unwrap(),
1153            Decimal::from_str("80.0").unwrap(),
1154        );
1155        book.apply_ask_delta(
1156            Decimal::from_str("0.77").unwrap(),
1157            Decimal::from_str("120.0").unwrap(),
1158        );
1159
1160        // Test liquidity at specific price - when buying, we look at ask liquidity
1161        let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY);
1162        assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap());
1163
1164        // Test liquidity at specific price - when selling, we look at bid liquidity
1165        let sell_liquidity =
1166            book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
1167        assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap());
1168
1169        // Test liquidity in range - when buying, we look at ask liquidity in range
1170        let buy_range_liquidity = book.liquidity_in_range(
1171            Decimal::from_str("0.74").unwrap(),
1172            Decimal::from_str("0.77").unwrap(),
1173            Side::BUY,
1174        );
1175        // Should include ask liquidity: 80 (0.76 ask) + 120 (0.77 ask) = 200
1176        assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap());
1177
1178        // Test liquidity in range - when selling, we look at bid liquidity in range
1179        let sell_range_liquidity = book.liquidity_in_range(
1180            Decimal::from_str("0.74").unwrap(),
1181            Decimal::from_str("0.77").unwrap(),
1182            Side::SELL,
1183        );
1184        // Should include bid liquidity: 50 (0.74 bid) + 100 (0.75 bid) = 150
1185        assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap());
1186    }
1187
1188    #[test]
1189    fn test_book_validation() {
1190        let mut book = OrderBook::new("test_token".to_string(), 10);
1191
1192        // Empty book should be valid
1193        assert!(book.is_valid());
1194
1195        // Add normal levels
1196        book.apply_bid_delta(
1197            Decimal::from_str("0.75").unwrap(),
1198            Decimal::from_str("100.0").unwrap(),
1199        );
1200        book.apply_ask_delta(
1201            Decimal::from_str("0.76").unwrap(),
1202            Decimal::from_str("80.0").unwrap(),
1203        );
1204        assert!(book.is_valid());
1205
1206        // Create crossed book (invalid) - bid higher than ask
1207        book.apply_bid_delta(
1208            Decimal::from_str("0.77").unwrap(),
1209            Decimal::from_str("50.0").unwrap(),
1210        );
1211        assert!(!book.is_valid());
1212    }
1213
1214    #[test]
1215    fn test_book_staleness() {
1216        let mut book = OrderBook::new("test_token".to_string(), 10);
1217
1218        // Fresh book should not be stale
1219        assert!(!book.is_stale(Duration::from_secs(60))); // 60 second threshold
1220
1221        // Add some data
1222        book.apply_bid_delta(
1223            Decimal::from_str("0.75").unwrap(),
1224            Decimal::from_str("100.0").unwrap(),
1225        );
1226        assert!(!book.is_stale(Duration::from_secs(60)));
1227
1228        // Note: We can't easily test actual staleness without manipulating time,
1229        // but we can test the method exists and works with fresh data
1230    }
1231
1232    #[test]
1233    fn test_depth_management() {
1234        let mut book = OrderBook::new("test_token".to_string(), 3); // Only 3 levels
1235
1236        // Add multiple levels
1237        book.apply_bid_delta(
1238            Decimal::from_str("0.75").unwrap(),
1239            Decimal::from_str("100.0").unwrap(),
1240        );
1241        book.apply_bid_delta(
1242            Decimal::from_str("0.74").unwrap(),
1243            Decimal::from_str("50.0").unwrap(),
1244        );
1245        book.apply_bid_delta(
1246            Decimal::from_str("0.73").unwrap(),
1247            Decimal::from_str("20.0").unwrap(),
1248        );
1249
1250        book.apply_ask_delta(
1251            Decimal::from_str("0.76").unwrap(),
1252            Decimal::from_str("80.0").unwrap(),
1253        );
1254        book.apply_ask_delta(
1255            Decimal::from_str("0.77").unwrap(),
1256            Decimal::from_str("40.0").unwrap(),
1257        );
1258        book.apply_ask_delta(
1259            Decimal::from_str("0.78").unwrap(),
1260            Decimal::from_str("30.0").unwrap(),
1261        );
1262
1263        // Should have levels on each side
1264        let bids = book.bids(Some(3));
1265        let asks = book.asks(Some(3));
1266
1267        assert!(bids.len() <= 3);
1268        assert!(asks.len() <= 3);
1269
1270        // Best levels should be there
1271        assert_eq!(
1272            book.best_bid().unwrap().price,
1273            Decimal::from_str("0.75").unwrap()
1274        );
1275        assert_eq!(
1276            book.best_ask().unwrap().price,
1277            Decimal::from_str("0.76").unwrap()
1278        );
1279    }
1280
1281    #[test]
1282    fn test_fast_operations() {
1283        let mut book = OrderBook::new("test_token".to_string(), 10);
1284
1285        // Test using legacy methods which call fast operations internally
1286        book.apply_bid_delta(
1287            Decimal::from_str("0.75").unwrap(),
1288            Decimal::from_str("100.0").unwrap(),
1289        );
1290        book.apply_ask_delta(
1291            Decimal::from_str("0.76").unwrap(),
1292            Decimal::from_str("80.0").unwrap(),
1293        );
1294
1295        let best_bid_fast = book.best_bid_fast();
1296        let best_ask_fast = book.best_ask_fast();
1297
1298        assert!(best_bid_fast.is_some());
1299        assert!(best_ask_fast.is_some());
1300
1301        // Test fast spread and mid price
1302        let spread_fast = book.spread_fast();
1303        let mid_fast = book.mid_price_fast();
1304
1305        assert!(spread_fast.is_some()); // Should have a spread
1306        assert!(mid_fast.is_some()); // Should have a mid price
1307    }
1308}