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 parking_lot::RwLock;
8use rust_decimal::Decimal;
9use std::collections::HashMap;
10use std::sync::Arc; // For shared access across multiple tasks
11use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14struct StoredLevel {
15    qty: Qty,
16    generation: u64,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub(crate) struct ParsedBookLevel {
21    pub side: Side,
22    pub price_ticks: Price,
23    pub size_units: Qty,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum BookSideKind {
28    Bid,
29    Ask,
30}
31
32#[derive(Debug, Clone)]
33struct BookSide {
34    levels: Vec<(Price, StoredLevel)>,
35    kind: BookSideKind,
36}
37
38impl BookSide {
39    fn new(kind: BookSideKind, max_depth: usize) -> Self {
40        Self {
41            levels: Vec::with_capacity(max_depth.saturating_add(8)),
42            kind,
43        }
44    }
45
46    #[inline]
47    fn len(&self) -> usize {
48        self.levels.len()
49    }
50
51    #[inline]
52    fn search(&self, price: Price) -> std::result::Result<usize, usize> {
53        match self.kind {
54            // Bids are stored highest price first.
55            BookSideKind::Bid => self
56                .levels
57                .binary_search_by(|(level_price, _)| price.cmp(level_price)),
58            // Asks are stored lowest price first.
59            BookSideKind::Ask => self
60                .levels
61                .binary_search_by(|(level_price, _)| level_price.cmp(&price)),
62        }
63    }
64
65    #[inline]
66    fn best(&self) -> Option<(Price, &StoredLevel)> {
67        self.levels.first().map(|(price, level)| (*price, level))
68    }
69
70    #[inline]
71    fn get(&self, price: Price) -> Option<&StoredLevel> {
72        self.search(price)
73            .ok()
74            .and_then(|idx| self.levels.get(idx).map(|(_, level)| level))
75    }
76
77    #[inline]
78    fn upsert(&mut self, price: Price, level: StoredLevel) {
79        match self.search(price) {
80            Ok(idx) if level.qty == 0 => {
81                self.levels.remove(idx);
82            },
83            Ok(idx) => {
84                self.levels[idx].1 = level;
85            },
86            Err(idx) if level.qty != 0 => {
87                self.levels.insert(idx, (price, level));
88            },
89            Err(_) => {},
90        }
91    }
92
93    #[inline]
94    fn retain_generation(&mut self, generation: u64) {
95        self.levels
96            .retain(|(_, level)| level.generation == generation);
97    }
98
99    #[inline]
100    fn trim_depth(&mut self, max_depth: usize) {
101        if self.levels.len() > max_depth {
102            self.levels.truncate(max_depth);
103        }
104    }
105
106    #[inline]
107    fn iter_top(&self, depth: usize) -> impl Iterator<Item = (Price, &StoredLevel)> {
108        self.levels
109            .iter()
110            .take(depth)
111            .map(|(price, level)| (*price, level))
112    }
113
114    #[inline]
115    fn iter_all(&self) -> impl Iterator<Item = (Price, &StoredLevel)> {
116        self.levels.iter().map(|(price, level)| (*price, level))
117    }
118
119    fn sum_range(&self, min_price: Price, max_price: Price) -> Qty {
120        let mut total = 0;
121        for (price, level) in &self.levels {
122            match self.kind {
123                BookSideKind::Bid => {
124                    if *price > max_price {
125                        continue;
126                    }
127                    if *price < min_price {
128                        break;
129                    }
130                },
131                BookSideKind::Ask => {
132                    if *price < min_price {
133                        continue;
134                    }
135                    if *price > max_price {
136                        break;
137                    }
138                },
139            }
140
141            total += level.qty;
142        }
143        total
144    }
145}
146
147const DEFAULT_BOOK_SHARDS: usize = 64;
148
149/// High-performance order book implementation
150///
151/// This is the core data structure that holds all the live buy/sell orders for a token.
152/// The efficiency of this code is critical as the order book is constantly being updated as orders are added and removed.
153///
154/// PERFORMANCE OPTIMIZATION: This struct now uses fixed-point integers internally
155/// instead of Decimal for maximum speed. The performance difference is dramatic:
156///
157/// Before (Decimal):  ~100ns per operation + memory allocation
158/// After (fixed-point): ~5ns per operation, zero allocations
159
160#[derive(Debug, Clone)]
161pub struct OrderBook {
162    /// Token ID this book represents (like "123456" for a specific prediction market outcome)
163    pub token_id: String,
164
165    /// Hash of token_id for fast lookups (avoids string comparisons in hot path)
166    pub token_id_hash: u64,
167
168    /// Legacy delta sequence number.
169    ///
170    /// Kept as a compatibility alias for [`Self::last_delta_sequence`]. WebSocket
171    /// snapshot timestamps are tracked separately in [`Self::last_snapshot_timestamp_ms`].
172    pub sequence: u64,
173
174    /// Last accepted legacy/incremental delta sequence number.
175    ///
176    /// Delta sequences are a different clock from WebSocket snapshot timestamps and
177    /// must not be compared against millisecond timestamps.
178    pub last_delta_sequence: u64,
179
180    /// Last accepted full-book snapshot timestamp in milliseconds.
181    ///
182    /// Used for WebSocket `book` snapshots and serde `BookUpdate` snapshots.
183    pub last_snapshot_timestamp_ms: u64,
184
185    /// Last update timestamp - when we last got new data for this book
186    pub timestamp: chrono::DateTime<Utc>,
187
188    /// Bid side (price -> size, sorted descending) - NOW USING FIXED-POINT!
189    /// Stored as a sorted vector with highest bids first for cache-local top-of-book iteration.
190    /// Key = price in ticks (like 6500 for $0.65), Value = size in fixed-point units
191    ///
192    /// BEFORE (slow): bids: BTreeMap<Decimal, Decimal>,
193    /// AFTER (fast):  bids: sorted Vec<(Price, StoredLevel)>,
194    ///
195    /// Why this is faster:
196    /// - Integer comparisons are ~10x faster than Decimal comparisons
197    /// - No memory allocation for each price level
198    /// - Better CPU cache utilization (smaller data structures)
199    bids: BookSide,
200
201    /// Ask side (price -> size, sorted ascending) - NOW USING FIXED-POINT!
202    /// Stored as a sorted vector with lowest asks first - people selling at cheapest prices.
203    ///
204    /// BEFORE (slow): asks: BTreeMap<Decimal, Decimal>,
205    /// AFTER (fast):  asks: sorted Vec<(Price, StoredLevel)>,
206    asks: BookSide,
207
208    /// Snapshot generation used to retain book levels without rescanning input payloads.
209    snapshot_generation: u64,
210
211    /// Last accepted full-book snapshot hash fingerprint, when the feed provides one.
212    ///
213    /// This is used as a tie-breaker for same-millisecond snapshots. If the timestamp is equal
214    /// but the hash fingerprint differs, the snapshot can still represent a newer book state.
215    last_snapshot_hash_fingerprint: Option<u64>,
216
217    /// Minimum tick size for this market in ticks (like 10 for $0.001 increments)
218    /// Some markets only allow certain price increments
219    /// We store this in ticks for fast validation without conversion
220    tick_size_ticks: Option<Price>,
221
222    /// Maximum depth to maintain (how many price levels to keep)
223    ///
224    /// We don't need to track every single price level, just the best ones because:
225    /// - Trading reality 90% of volume happens in the top 5-10 price levels
226    /// - Execution priority: Orders get filled from best price first, so deep levels often don't matter
227    /// - Market efficiency: If you're buying and best ask is $0.67, you'll never pay $0.95
228    /// - Risk management: Large orders that would hit deep levels are usually broken up
229    /// - Data freshness: Deep levels often have stale orders from hours/days ago
230    ///
231    /// Typical values: 10-50 for retail, 100-500 for institutional HFT systems
232    max_depth: usize,
233}
234
235impl OrderBook {
236    /// Create a new order book
237    /// Just sets up empty bid/ask maps and basic metadata
238    pub fn new(token_id: String, max_depth: usize) -> Self {
239        // Hash the token_id once for fast lookups later
240        let token_id_hash = {
241            use std::collections::hash_map::DefaultHasher;
242            use std::hash::{Hash, Hasher};
243            let mut hasher = DefaultHasher::new();
244            token_id.hash(&mut hasher);
245            hasher.finish()
246        };
247
248        Self {
249            token_id,
250            token_id_hash,
251            sequence: 0, // Compatibility alias for last_delta_sequence
252            last_delta_sequence: 0,
253            last_snapshot_timestamp_ms: 0,
254            timestamp: Utc::now(),
255            bids: BookSide::new(BookSideKind::Bid, max_depth), // Empty to start
256            asks: BookSide::new(BookSideKind::Ask, max_depth), // Empty to start
257            snapshot_generation: 0,
258            last_snapshot_hash_fingerprint: None,
259            tick_size_ticks: None, // We'll set this later when we learn about the market
260            max_depth,
261        }
262    }
263
264    /// Set the tick size for this book
265    /// This tells us the minimum price increment allowed
266    /// We store it in ticks for fast validation without conversion overhead
267    pub fn set_tick_size(&mut self, tick_size: Decimal) -> Result<()> {
268        let tick_size_ticks = decimal_to_price_exact(tick_size)
269            .map_err(|_| PolyfillError::validation("Invalid tick size"))?;
270        self.tick_size_ticks = Some(tick_size_ticks);
271        Ok(())
272    }
273
274    /// Set the tick size directly in ticks (even faster)
275    /// Use this when you already have the tick size in our internal format
276    pub fn set_tick_size_ticks(&mut self, tick_size_ticks: Price) {
277        self.tick_size_ticks = Some(tick_size_ticks);
278    }
279
280    /// Get the current best bid (highest price someone is willing to pay)
281    /// Bids are stored highest price first.
282    ///
283    /// PERFORMANCE: Now returns data in external format but internally uses fast lookups
284    pub fn best_bid(&self) -> Option<BookLevel> {
285        // BEFORE (slow, ~50ns + allocation):
286        // self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size })
287
288        // AFTER (fast, ~5ns, no allocation for the lookup):
289        self.bids.best().map(|(price_ticks, level)| {
290            // Convert from internal fixed-point to external Decimal format
291            // This conversion only happens at the API boundary
292            BookLevel {
293                price: price_to_decimal(price_ticks),
294                size: qty_to_decimal(level.qty),
295            }
296        })
297    }
298
299    /// Get the current best ask (lowest price someone is willing to sell at)
300    /// Asks are stored lowest price first.
301    ///
302    /// PERFORMANCE: Now returns data in external format but internally uses fast lookups
303    pub fn best_ask(&self) -> Option<BookLevel> {
304        // BEFORE (slow, ~50ns + allocation):
305        // self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
306
307        // AFTER (fast, ~5ns, no allocation for the lookup):
308        self.asks.best().map(|(price_ticks, level)| {
309            // Convert from internal fixed-point to external Decimal format
310            // This conversion only happens at the API boundary
311            BookLevel {
312                price: price_to_decimal(price_ticks),
313                size: qty_to_decimal(level.qty),
314            }
315        })
316    }
317
318    /// Get the current best bid in fast internal format
319    /// Use this for internal calculations to avoid conversion overhead
320    pub fn best_bid_fast(&self) -> Option<FastBookLevel> {
321        self.bids
322            .best()
323            .map(|(price, level)| FastBookLevel::new(price, level.qty))
324    }
325
326    /// Get the current best ask in fast internal format
327    /// Use this for internal calculations to avoid conversion overhead
328    pub fn best_ask_fast(&self) -> Option<FastBookLevel> {
329        self.asks
330            .best()
331            .map(|(price, level)| FastBookLevel::new(price, level.qty))
332    }
333
334    /// Get the current spread (difference between best ask and best bid)
335    /// This tells us how "tight" the market is - smaller spread = more liquid market
336    ///
337    /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
338    pub fn spread(&self) -> Option<Decimal> {
339        // BEFORE (slow, ~100ns + multiple allocations):
340        // match (self.best_bid(), self.best_ask()) {
341        //     (Some(bid), Some(ask)) => Some(ask.price - bid.price),
342        //     _ => None,
343        // }
344
345        // AFTER (fast, ~5ns, no allocations):
346        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
347        let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?;
348        Some(price_to_decimal(spread_ticks))
349    }
350
351    /// Get the current mid price (halfway between best bid and ask)
352    /// This is often used as the "fair value" of the market
353    ///
354    /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
355    pub fn mid_price(&self) -> Option<Decimal> {
356        // BEFORE (slow, ~80ns + allocations):
357        // math::mid_price(
358        //     self.best_bid()?.price,
359        //     self.best_ask()?.price,
360        // )
361
362        // AFTER (fast, ~3ns, no allocations):
363        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
364        let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?;
365        Some(price_to_decimal(mid_ticks))
366    }
367
368    /// Get the spread as a percentage (relative to the bid price)
369    /// Useful for comparing spreads across different price levels
370    ///
371    /// PERFORMANCE: Now uses fast internal calculations and returns basis points
372    pub fn spread_pct(&self) -> Option<Decimal> {
373        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
374        let spread_bps = math::spread_pct_fast(best_bid_ticks, best_ask_ticks)?;
375        // Convert basis points back to percentage decimal
376        Some(Decimal::from(spread_bps) / Decimal::from(100))
377    }
378
379    /// Get best bid and ask prices in fast internal format
380    /// Helper method to avoid code duplication and minimize conversions
381    fn best_prices_fast(&self) -> Option<(Price, Price)> {
382        let best_bid_ticks = self.bids.best()?.0;
383        let best_ask_ticks = self.asks.best()?.0;
384        Some((best_bid_ticks, best_ask_ticks))
385    }
386
387    /// Get the current spread in fast internal format (PERFORMANCE OPTIMIZED)
388    /// Returns spread in ticks - use this for internal calculations
389    pub fn spread_fast(&self) -> Option<Price> {
390        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
391        math::spread_fast(best_bid_ticks, best_ask_ticks)
392    }
393
394    /// Get the current mid price in fast internal format (PERFORMANCE OPTIMIZED)
395    /// Returns mid price in ticks - use this for internal calculations
396    pub fn mid_price_fast(&self) -> Option<Price> {
397        let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
398        math::mid_price_fast(best_bid_ticks, best_ask_ticks)
399    }
400
401    /// Get all bids up to a certain depth (top N price levels)
402    /// Returns them in descending price order (best bids first)
403    ///
404    /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
405    /// Only call this when you need to return data to external APIs
406    pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
407        let depth = depth.unwrap_or(self.max_depth);
408        self.bids
409            .iter_top(depth)
410            .map(|(price_ticks, level)| BookLevel {
411                price: price_to_decimal(price_ticks),
412                size: qty_to_decimal(level.qty),
413            })
414            .collect()
415    }
416
417    /// Get all asks up to a certain depth (top N price levels)
418    /// Returns them in ascending price order (best asks first)
419    ///
420    /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
421    /// Only call this when you need to return data to external APIs
422    pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
423        let depth = depth.unwrap_or(self.max_depth);
424        self.asks
425            .iter_top(depth)
426            .map(|(price_ticks, level)| BookLevel {
427                price: price_to_decimal(price_ticks),
428                size: qty_to_decimal(level.qty),
429            })
430            .collect()
431    }
432
433    /// Get all bids in fast internal format
434    /// Use this for internal calculations to avoid conversion overhead
435    pub fn bids_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
436        let depth = depth.unwrap_or(self.max_depth);
437        self.bids
438            .iter_top(depth)
439            .map(|(price, level)| FastBookLevel::new(price, level.qty))
440            .collect()
441    }
442
443    /// Get all asks in fast internal format (PERFORMANCE OPTIMIZED)
444    /// Use this for internal calculations to avoid conversion overhead
445    pub fn asks_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
446        let depth = depth.unwrap_or(self.max_depth);
447        self.asks
448            .iter_top(depth)
449            .map(|(price, level)| FastBookLevel::new(price, level.qty))
450            .collect()
451    }
452
453    /// Get the full book snapshot
454    /// Creates a copy of the current state that can be safely passed around
455    /// without worrying about the original book changing
456    pub fn snapshot(&self) -> crate::types::OrderBook {
457        crate::types::OrderBook {
458            token_id: self.token_id.clone(),
459            timestamp: self.timestamp,
460            bids: self.bids(None), // Get all bids (up to max_depth)
461            asks: self.asks(None), // Get all asks (up to max_depth)
462            sequence: self.last_delta_sequence,
463            last_delta_sequence: self.last_delta_sequence,
464            last_snapshot_timestamp_ms: self.last_snapshot_timestamp_ms,
465        }
466    }
467
468    /// Apply a delta update to the book (LEGACY VERSION - for external API compatibility)
469    /// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70"
470    ///
471    /// This method converts the external Decimal delta to our internal fixed-point format
472    /// and then calls the fast version. Use apply_delta_fast() directly when possible.
473    pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
474        // Convert to fast internal format with tick alignment validation
475        let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal);
476        let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal)
477            .map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?;
478
479        // Use the fast internal version
480        self.apply_delta_fast(fast_delta)
481    }
482
483    /// Apply a delta update to the book
484    ///
485    /// This is the high-performance version that works directly with fixed-point data.
486    /// It includes tick alignment validation and is much faster than the Decimal version.
487    ///
488    /// Performance improvement: ~50x faster than the old Decimal version!
489    /// - No Decimal conversions in the hot path
490    /// - Integer comparisons instead of Decimal comparisons
491    /// - No memory allocations for price/size operations
492    pub fn apply_delta_fast(&mut self, delta: FastOrderDelta) -> Result<()> {
493        // Validate sequence ordering - ignore old updates that arrive late
494        // This is crucial for maintaining data integrity in real-time systems
495        if delta.sequence <= self.last_delta_sequence {
496            trace!(
497                "Ignoring stale delta: {} <= {}",
498                delta.sequence,
499                self.last_delta_sequence
500            );
501            return Ok(());
502        }
503
504        // Validate token ID hash matches (fast string comparison avoidance)
505        if delta.token_id_hash != self.token_id_hash {
506            return Err(PolyfillError::validation("Token ID mismatch"));
507        }
508
509        // TICK ALIGNMENT VALIDATION - this is where we enforce price rules
510        // If we have a tick size, make sure the price aligns properly
511        if let Some(tick_size_ticks) = self.tick_size_ticks {
512            // BEFORE (slow, ~200ns + multiple conversions):
513            // let tick_size_decimal = price_to_decimal(tick_size_ticks);
514            // if !is_price_tick_aligned(price_to_decimal(delta.price), tick_size_decimal) {
515            //     return Err(...);
516            // }
517
518            // AFTER (fast, ~2ns, pure integer):
519            if tick_size_ticks > 0 && !delta.price.is_multiple_of(tick_size_ticks) {
520                // Price is not aligned to tick size - reject the update
521                warn!(
522                    "Rejecting misaligned price: {} not divisible by tick size {}",
523                    delta.price, tick_size_ticks
524                );
525                return Err(PolyfillError::validation("Price not aligned to tick size"));
526            }
527        }
528
529        // Update our tracking info
530        self.last_delta_sequence = delta.sequence;
531        self.sequence = delta.sequence;
532        self.timestamp = delta.timestamp;
533
534        // Apply the actual change to the appropriate side (FAST VERSION)
535        match delta.side {
536            Side::BUY => self.apply_bid_delta_fast(delta.price, delta.size),
537            Side::SELL => self.apply_ask_delta_fast(delta.price, delta.size),
538        }
539
540        // Keep the book from getting too deep (memory management)
541        self.trim_depth();
542
543        debug!(
544            "Applied fast delta: {} {} @ {} ticks (seq: {})",
545            delta.side.as_str(),
546            delta.size,
547            delta.price,
548            delta.sequence
549        );
550
551        Ok(())
552    }
553
554    /// Return whether a WebSocket `book` snapshot should be applied.
555    pub(crate) fn should_apply_ws_book_update(
556        &self,
557        asset_id: &str,
558        timestamp: u64,
559        hash: Option<&str>,
560    ) -> Result<bool> {
561        if asset_id != self.token_id {
562            return Err(PolyfillError::validation("Token ID mismatch"));
563        }
564
565        Ok(self.should_apply_snapshot(timestamp, hash))
566    }
567
568    /// Atomically apply a WebSocket `book` snapshot.
569    ///
570    /// The caller should parse levels into `ParsedBookLevel`s before calling this method. This
571    /// method validates tick alignment before mutating sequence/generation or book levels, so a
572    /// malformed snapshot leaves the existing book unchanged.
573    pub(crate) fn apply_ws_book_snapshot_fast(
574        &mut self,
575        asset_id: &str,
576        timestamp: u64,
577        hash: Option<&str>,
578        levels: &[ParsedBookLevel],
579    ) -> Result<bool> {
580        if !self.should_apply_ws_book_update(asset_id, timestamp, hash)? {
581            return Ok(false);
582        }
583
584        self.apply_validated_snapshot(timestamp, hash, levels)?;
585
586        Ok(true)
587    }
588
589    /// Apply a WebSocket `book` update for this token.
590    ///
591    /// The official Polymarket CLOB WebSocket `book` event is a full snapshot.
592    /// Unlike `apply_delta_fast`, this method can apply many levels that share
593    /// the same message timestamp.
594    ///
595    /// Notes:
596    /// - This replaces the snapshot for both sides.
597    /// - Levels omitted from the message are removed.
598    /// - Insertions of *new* price levels may allocate or shift vector entries.
599    pub fn apply_book_update(&mut self, update: &BookUpdate) -> Result<()> {
600        if update.asset_id != self.token_id {
601            return Err(PolyfillError::validation("Token ID mismatch"));
602        }
603
604        // Use the exchange-provided timestamp as the primary snapshot marker. The WS `book`
605        // message does not expose a monotonic server sequence/version, so same-millisecond
606        // snapshots with different hashes are ordered by websocket arrival order. The hash
607        // distinguishes duplicate vs distinct state; it is not a logical ordering key.
608        // Snapshot timestamps are intentionally separate from legacy delta sequence numbers.
609        if !self.should_apply_snapshot(update.timestamp, update.hash.as_deref()) {
610            return Ok(());
611        }
612
613        // Validate the whole snapshot before mutating this book. A malformed level must not leave
614        // behind a partial generation or an advanced sequence number.
615        for level in &update.bids {
616            let parsed = self.parse_snapshot_summary(Side::BUY, level)?;
617            self.validate_snapshot_level(parsed)?;
618        }
619
620        for level in &update.asks {
621            let parsed = self.parse_snapshot_summary(Side::SELL, level)?;
622            self.validate_snapshot_level(parsed)?;
623        }
624
625        self.last_snapshot_timestamp_ms = update.timestamp;
626        self.last_snapshot_hash_fingerprint = update.hash.as_deref().map(snapshot_hash_fingerprint);
627        self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(update.timestamp as i64)
628            .unwrap_or_else(Utc::now);
629        self.begin_snapshot();
630
631        // Re-parse after validation to preserve the existing no-allocation behavior for
632        // `BookUpdate` snapshots. Decimal conversion is deterministic, so these conversions cannot
633        // fail after the validation pass above.
634        for level in &update.bids {
635            let parsed = self
636                .parse_snapshot_summary(Side::BUY, level)
637                .expect("book update bid level was validated before mutation");
638            self.apply_snapshot_level(parsed.side, parsed.price_ticks, parsed.size_units);
639        }
640
641        for level in &update.asks {
642            let parsed = self
643                .parse_snapshot_summary(Side::SELL, level)
644                .expect("book update ask level was validated before mutation");
645            self.apply_snapshot_level(parsed.side, parsed.price_ticks, parsed.size_units);
646        }
647
648        self.finish_snapshot();
649        self.trim_depth();
650        Ok(())
651    }
652
653    /// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION
654    /// If size is 0, it means "remove this price level entirely"
655    /// Otherwise, set the total size at this price level
656    ///
657    /// This converts to fixed-point and calls the fast version
658    #[allow(dead_code)]
659    fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
660        // Convert to fixed-point (this should be rare since we use fast path)
661        let price_ticks = decimal_to_price_lossy(price).unwrap_or(0);
662        let size_units = decimal_to_qty(size).unwrap_or(0);
663        self.apply_bid_delta_fast(price_ticks, size_units);
664    }
665
666    /// Apply an ask-side delta (someone wants to sell) - LEGACY VERSION
667    /// Same logic as bids - size of 0 means remove the price level
668    ///
669    /// This converts to fixed-point and calls the fast version
670    #[allow(dead_code)]
671    fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
672        // Convert to fixed-point (this should be rare since we use fast path)
673        let price_ticks = decimal_to_price_lossy(price).unwrap_or(0);
674        let size_units = decimal_to_qty(size).unwrap_or(0);
675        self.apply_ask_delta_fast(price_ticks, size_units);
676    }
677
678    /// Apply a bid-side delta (someone wants to buy) - FAST VERSION
679    ///
680    /// This is the high-performance version that works directly with fixed-point.
681    /// Much faster than the Decimal version - pure integer operations.
682    fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
683        // BEFORE (slow, ~100ns + allocation):
684        // if size.is_zero() {
685        //     self.bids.remove(&price);
686        // } else {
687        //     self.bids.insert(price, size);
688        // }
689
690        // AFTER (fast, ~5ns, no allocation):
691        self.bids.upsert(
692            price_ticks,
693            StoredLevel {
694                qty: size_units,
695                generation: self.snapshot_generation,
696            },
697        );
698    }
699
700    /// Apply an ask-side delta (someone wants to sell) - FAST VERSION
701    ///
702    /// This is the high-performance version that works directly with fixed-point.
703    /// Much faster than the Decimal version - pure integer operations.
704    fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
705        // BEFORE (slow, ~100ns + allocation):
706        // if size.is_zero() {
707        //     self.asks.remove(&price);
708        // } else {
709        //     self.asks.insert(price, size);
710        // }
711
712        // AFTER (fast, ~5ns, no allocation):
713        self.asks.upsert(
714            price_ticks,
715            StoredLevel {
716                qty: size_units,
717                generation: self.snapshot_generation,
718            },
719        );
720    }
721
722    #[inline]
723    fn begin_snapshot(&mut self) {
724        self.snapshot_generation = self.snapshot_generation.wrapping_add(1);
725    }
726
727    #[inline]
728    fn should_apply_snapshot(&self, timestamp: u64, hash: Option<&str>) -> bool {
729        // Without a server sequence/version, equal-timestamp distinct hashes can only be ordered
730        // by arrival. Older timestamps are always stale; exact same timestamp/hash is duplicate.
731        if timestamp > self.last_snapshot_timestamp_ms {
732            return true;
733        }
734        if timestamp < self.last_snapshot_timestamp_ms {
735            return false;
736        }
737
738        hash.is_some_and(|hash| {
739            self.last_snapshot_hash_fingerprint != Some(snapshot_hash_fingerprint(hash))
740        })
741    }
742
743    #[inline]
744    fn parse_snapshot_summary(&self, side: Side, level: &OrderSummary) -> Result<ParsedBookLevel> {
745        let price_ticks = decimal_to_price_exact(level.price)
746            .map_err(|_| PolyfillError::validation("Invalid price"))?;
747        let size_units =
748            decimal_to_qty(level.size).map_err(|_| PolyfillError::validation("Invalid size"))?;
749
750        Ok(ParsedBookLevel {
751            side,
752            price_ticks,
753            size_units,
754        })
755    }
756
757    #[inline]
758    fn validate_snapshot_level(&self, level: ParsedBookLevel) -> Result<()> {
759        if let Some(tick_size_ticks) = self.tick_size_ticks {
760            if tick_size_ticks > 0 && !level.price_ticks.is_multiple_of(tick_size_ticks) {
761                return Err(PolyfillError::validation("Price not aligned to tick size"));
762            }
763        }
764
765        Ok(())
766    }
767
768    fn apply_validated_snapshot(
769        &mut self,
770        timestamp: u64,
771        hash: Option<&str>,
772        levels: &[ParsedBookLevel],
773    ) -> Result<()> {
774        for &level in levels {
775            self.validate_snapshot_level(level)?;
776        }
777
778        self.last_snapshot_timestamp_ms = timestamp;
779        self.last_snapshot_hash_fingerprint = hash.map(snapshot_hash_fingerprint);
780        self.timestamp = chrono::DateTime::<Utc>::from_timestamp_millis(timestamp as i64)
781            .unwrap_or_else(Utc::now);
782        self.begin_snapshot();
783
784        for &level in levels {
785            self.apply_snapshot_level(level.side, level.price_ticks, level.size_units);
786        }
787
788        self.finish_snapshot();
789        self.trim_depth();
790
791        Ok(())
792    }
793
794    #[inline]
795    fn apply_snapshot_level(&mut self, side: Side, price_ticks: Price, size_units: Qty) {
796        let generation = self.snapshot_generation;
797        let book_side = match side {
798            Side::BUY => &mut self.bids,
799            Side::SELL => &mut self.asks,
800        };
801
802        book_side.upsert(
803            price_ticks,
804            StoredLevel {
805                qty: size_units,
806                generation,
807            },
808        );
809    }
810
811    #[inline]
812    fn finish_snapshot(&mut self) {
813        let generation = self.snapshot_generation;
814        self.bids.retain_generation(generation);
815        self.asks.retain_generation(generation);
816    }
817
818    /// Trim the book to maintain depth limits
819    /// We don't want to track every single price level - just the best ones
820    ///
821    /// Why limit depth? Several reasons:
822    /// 1. Memory efficiency: A popular market might have thousands of price levels,
823    ///    but only the top 10-50 levels are actually tradeable with reasonable size
824    /// 2. Performance: Fewer levels = faster iteration when calculating market impact
825    /// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
826    ///    mostly noise and will never get hit in normal trading
827    /// 4. Stale data: Deep levels often contain old orders that haven't been cancelled
828    /// 5. Network bandwidth: Less data to send when streaming updates
829    fn trim_depth(&mut self) {
830        // For bids, remove the LOWEST prices (worst bids) if we have too many
831        // Example: If best bid is $0.65, we don't care about bids at $0.10
832        self.bids.trim_depth(self.max_depth);
833
834        // For asks, remove the HIGHEST prices (worst asks) if we have too many
835        // Example: If best ask is $0.67, we don't care about asks at $0.95
836        self.asks.trim_depth(self.max_depth);
837    }
838
839    /// Calculate the market impact for a given order size
840    /// This is exactly why we don't need deep levels - if your order would require
841    /// hitting prices way off the current market (like $0.95 when best ask is $0.67),
842    /// you'd never actually place that order. You'd either:
843    /// 1. Break it into smaller pieces over time
844    /// 2. Use a different trading strategy
845    /// 3. Accept that there's not enough liquidity right now
846    pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
847        let size_units = decimal_to_qty(size).ok()?;
848        let (filled_units, total_notional, best_price_ticks) = match side {
849            Side::BUY => fill_market_impact(self.asks.iter_all(), size_units)?,
850            Side::SELL => fill_market_impact(self.bids.iter_all(), size_units)?,
851        };
852
853        let total_cost = Decimal::from_i128_with_scale(total_notional, 8);
854        let filled_size = qty_to_decimal(filled_units);
855        let avg_price = total_cost / filled_size;
856        let best_price = price_to_decimal(best_price_ticks);
857        let impact = if side == Side::BUY {
858            (avg_price - best_price) / best_price
859        } else {
860            (best_price - avg_price) / best_price
861        };
862
863        Some(MarketImpact {
864            average_price: avg_price,
865            impact_pct: impact,
866            total_cost,
867            size_filled: filled_size,
868        })
869    }
870
871    /// Check if the book is stale (no recent updates)
872    /// Useful for detecting when we've lost connection to live data
873    pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
874        let age = Utc::now() - self.timestamp;
875        age > chrono::Duration::from_std(max_age).unwrap_or_default()
876    }
877
878    /// Get the total liquidity at a given price level
879    /// Tells you how much you can buy/sell at exactly this price
880    pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
881        // Convert decimal price to our internal fixed-point representation
882        let price_ticks = match decimal_to_price_exact(price) {
883            Ok(ticks) => ticks,
884            Err(_) => return Decimal::ZERO, // Invalid price
885        };
886
887        match side {
888            Side::BUY => {
889                // How much we can buy at this price (look at asks)
890                let size_units = self
891                    .asks
892                    .get(price_ticks)
893                    .map(|level| level.qty)
894                    .unwrap_or_default();
895                qty_to_decimal(size_units)
896            },
897            Side::SELL => {
898                // How much we can sell at this price (look at bids)
899                let size_units = self
900                    .bids
901                    .get(price_ticks)
902                    .map(|level| level.qty)
903                    .unwrap_or_default();
904                qty_to_decimal(size_units)
905            },
906        }
907    }
908
909    /// Get the total liquidity within a price range
910    /// Useful for understanding how much depth exists in a certain price band
911    pub fn liquidity_in_range(
912        &self,
913        min_price: Decimal,
914        max_price: Decimal,
915        side: Side,
916    ) -> Decimal {
917        // Convert decimal prices to our internal fixed-point representation
918        let min_price_ticks = match decimal_to_price_exact(min_price) {
919            Ok(ticks) => ticks,
920            Err(_) => return Decimal::ZERO, // Invalid price
921        };
922        let max_price_ticks = match decimal_to_price_exact(max_price) {
923            Ok(ticks) => ticks,
924            Err(_) => return Decimal::ZERO, // Invalid price
925        };
926
927        let total_size_units: Qty = match side {
928            Side::BUY => self.asks.sum_range(min_price_ticks, max_price_ticks),
929            Side::SELL => self.bids.sum_range(min_price_ticks, max_price_ticks),
930        };
931
932        qty_to_decimal(total_size_units)
933    }
934
935    /// Validate that prices are properly ordered
936    /// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
937    pub fn is_valid(&self) -> bool {
938        self.best_prices_fast()
939            .map(|(best_bid_ticks, best_ask_ticks)| best_bid_ticks < best_ask_ticks)
940            .unwrap_or(true)
941    }
942}
943
944fn fill_market_impact<'a>(
945    levels: impl Iterator<Item = (Price, &'a StoredLevel)>,
946    size_units: Qty,
947) -> Option<(Qty, i128, Price)> {
948    if size_units <= 0 {
949        return None;
950    }
951
952    let mut remaining_units = size_units;
953    let mut filled_units = 0;
954    let mut total_notional = 0i128;
955    let mut best_price_ticks = None;
956
957    for (price_ticks, level) in levels {
958        if best_price_ticks.is_none() {
959            best_price_ticks = Some(price_ticks);
960        }
961
962        if level.qty <= 0 {
963            continue;
964        }
965
966        let fill_units = remaining_units.min(level.qty);
967        let fill_notional = (price_ticks as i128).checked_mul(fill_units as i128)?;
968        total_notional = total_notional.checked_add(fill_notional)?;
969        filled_units += fill_units;
970        remaining_units -= fill_units;
971
972        if remaining_units == 0 {
973            break;
974        }
975    }
976
977    if remaining_units > 0 {
978        return None;
979    }
980
981    Some((filled_units, total_notional, best_price_ticks?))
982}
983
984/// Market impact calculation result
985/// This tells you what would happen if you executed a large order
986#[derive(Debug, Clone)]
987pub struct MarketImpact {
988    pub average_price: Decimal, // The average price you'd get across all fills
989    pub impact_pct: Decimal,    // How much worse than the best price (as percentage)
990    pub total_cost: Decimal,    // Total amount you'd pay/receive
991    pub size_filled: Decimal,   // How much of your order got filled
992}
993
994/// Thread-safe order book manager
995/// This manages multiple order books (one per token) and handles concurrent access
996/// Multiple threads can read/write different books simultaneously
997///
998/// The depth limiting becomes even more critical here because we might be tracking
999/// hundreds or thousands of different tokens simultaneously. If each book had
1000/// unlimited depth, we could easily use gigabytes of RAM for mostly useless data.
1001///
1002/// Example: 1000 tokens × 1000 price levels × 32 bytes per level = 32MB just for prices
1003/// With depth limiting: 1000 tokens × 50 levels × 32 bytes = 1.6MB (20x less memory)
1004#[derive(Debug)]
1005pub struct OrderBookManager {
1006    shards: Arc<[BookShard]>, // Token ID -> shard-local OrderBook
1007    max_depth: usize,
1008}
1009
1010#[derive(Debug, Default)]
1011struct BookShard {
1012    books: RwLock<HashMap<String, OrderBook>>,
1013}
1014
1015#[inline]
1016fn snapshot_hash_fingerprint(hash: &str) -> u64 {
1017    let mut fingerprint = 0xcbf2_9ce4_8422_2325u64;
1018    for &byte in hash.as_bytes() {
1019        fingerprint ^= byte as u64;
1020        fingerprint = fingerprint.wrapping_mul(0x0000_0100_0000_01b3);
1021    }
1022    fingerprint
1023}
1024
1025#[inline]
1026fn shard_index(token_id: &str, shard_count: usize) -> usize {
1027    debug_assert!(shard_count > 0);
1028    let mut hash = 0xcbf2_9ce4_8422_2325u64;
1029    for &byte in token_id.as_bytes() {
1030        hash ^= byte as u64;
1031        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1032    }
1033    (hash as usize) % shard_count
1034}
1035
1036impl OrderBookManager {
1037    /// Create a new order book manager
1038    /// Starts with an empty collection of books
1039    pub fn new(max_depth: usize) -> Self {
1040        Self::with_shard_count(max_depth, DEFAULT_BOOK_SHARDS)
1041    }
1042
1043    /// Create a new order book manager with an explicit shard count.
1044    ///
1045    /// This is primarily useful for tests and tuning. A shard count of zero is
1046    /// treated as one shard.
1047    pub fn with_shard_count(max_depth: usize, shard_count: usize) -> Self {
1048        let shard_count = shard_count.max(1);
1049        let shards = (0..shard_count)
1050            .map(|_| BookShard::default())
1051            .collect::<Vec<_>>()
1052            .into();
1053
1054        Self { shards, max_depth }
1055    }
1056
1057    #[inline]
1058    fn shard_for(&self, token_id: &str) -> &BookShard {
1059        &self.shards[shard_index(token_id, self.shards.len())]
1060    }
1061
1062    /// Get or create an order book for a token
1063    /// If we don't have a book for this token yet, create a new empty one
1064    pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
1065        let shard = self.shard_for(token_id);
1066        let mut books = shard.books.write();
1067
1068        if let Some(book) = books.get(token_id) {
1069            Ok(book.clone()) // Return a copy of the existing book
1070        } else {
1071            // Create a new book for this token
1072            let book = OrderBook::new(token_id.to_string(), self.max_depth);
1073            books.insert(token_id.to_string(), book.clone());
1074            Ok(book)
1075        }
1076    }
1077
1078    /// Execute a closure with mutable access to a managed book.
1079    ///
1080    /// This is useful for hot-path update ingestion where you want to avoid allocating
1081    /// intermediate update structs (e.g., applying WS updates directly).
1082    pub fn with_book_mut<R>(
1083        &self,
1084        token_id: &str,
1085        f: impl FnOnce(&mut OrderBook) -> Result<R>,
1086    ) -> Result<R> {
1087        let shard = self.shard_for(token_id);
1088        let mut books = shard.books.write();
1089
1090        let book = books.get_mut(token_id).ok_or_else(|| {
1091            PolyfillError::market_data(
1092                format!("No book found for token: {}", token_id),
1093                crate::errors::MarketDataErrorKind::TokenNotFound,
1094            )
1095        })?;
1096
1097        f(book)
1098    }
1099
1100    /// Update a book with a delta
1101    /// This is called when we receive real-time updates from the exchange
1102    pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
1103        let shard = self.shard_for(delta.token_id.as_str());
1104        let mut books = shard.books.write();
1105
1106        // Find the book for this token (must already exist)
1107        let book = books.get_mut(&delta.token_id).ok_or_else(|| {
1108            PolyfillError::market_data(
1109                format!("No book found for token: {}", delta.token_id),
1110                crate::errors::MarketDataErrorKind::TokenNotFound,
1111            )
1112        })?;
1113
1114        // Apply the update to the specific book
1115        book.apply_delta(delta)
1116    }
1117
1118    /// Apply a WebSocket `book` update to a managed book.
1119    ///
1120    /// This is the preferred way to ingest `StreamMessage::Book` updates into
1121    /// the in-memory order books (avoids rebuilding snapshots via per-level deltas).
1122    pub fn apply_book_update(&self, update: &BookUpdate) -> Result<()> {
1123        let shard = self.shard_for(update.asset_id.as_str());
1124        let mut books = shard.books.write();
1125
1126        if !books.contains_key(update.asset_id.as_str()) {
1127            let token_id = update.asset_id.clone();
1128            books.insert(token_id.clone(), OrderBook::new(token_id, self.max_depth));
1129        }
1130
1131        books
1132            .get_mut(update.asset_id.as_str())
1133            .ok_or_else(|| PolyfillError::internal_simple("Failed to insert order book"))?
1134            .apply_book_update(update)
1135    }
1136
1137    /// Get a book snapshot
1138    /// Returns a copy of the current book state that won't change
1139    pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
1140        let shard = self.shard_for(token_id);
1141        let books = shard.books.read();
1142
1143        books
1144            .get(token_id)
1145            .map(|book| book.snapshot()) // Create a snapshot copy
1146            .ok_or_else(|| {
1147                PolyfillError::market_data(
1148                    format!("No book found for token: {}", token_id),
1149                    crate::errors::MarketDataErrorKind::TokenNotFound,
1150                )
1151            })
1152    }
1153
1154    /// Get all available books
1155    /// Returns snapshots of every book we're currently tracking
1156    pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
1157        let mut snapshots = Vec::new();
1158        for shard in self.shards.iter() {
1159            let books = shard.books.read();
1160            snapshots.extend(books.values().map(|book| book.snapshot()));
1161        }
1162
1163        Ok(snapshots)
1164    }
1165
1166    /// Remove stale books
1167    /// Cleans up books that haven't been updated recently (probably disconnected)
1168    /// This prevents memory leaks from accumulating dead books
1169    pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
1170        let mut removed = 0usize;
1171
1172        for shard in self.shards.iter() {
1173            let mut books = shard.books.write();
1174            let initial_count = books.len();
1175            books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books
1176            removed += initial_count - books.len();
1177        }
1178
1179        if removed > 0 {
1180            debug!("Removed {} stale order books", removed);
1181        }
1182
1183        Ok(removed)
1184    }
1185}
1186
1187/// Order book analytics and statistics
1188/// Provides a summary view of the book's health and characteristics
1189#[derive(Debug, Clone)]
1190pub struct BookAnalytics {
1191    pub token_id: String,
1192    pub timestamp: chrono::DateTime<Utc>,
1193    pub bid_count: usize,            // How many different bid price levels
1194    pub ask_count: usize,            // How many different ask price levels
1195    pub total_bid_size: Decimal,     // Total size of all bids combined
1196    pub total_ask_size: Decimal,     // Total size of all asks combined
1197    pub spread: Option<Decimal>,     // Current spread (ask - bid)
1198    pub spread_pct: Option<Decimal>, // Spread as percentage
1199    pub mid_price: Option<Decimal>,  // Current mid price
1200    pub volatility: Option<Decimal>, // Price volatility (if calculated)
1201}
1202
1203impl OrderBook {
1204    /// Calculate analytics for this book
1205    /// Gives you a quick health check of the market
1206    pub fn analytics(&self) -> BookAnalytics {
1207        let bid_count = self.bids.len();
1208        let ask_count = self.asks.len();
1209        // Sum up all bid/ask sizes, converting from fixed-point back to Decimal
1210        let total_bid_size_units: i64 = self.bids.iter_all().map(|(_, level)| level.qty).sum();
1211        let total_ask_size_units: i64 = self.asks.iter_all().map(|(_, level)| level.qty).sum();
1212        let total_bid_size = qty_to_decimal(total_bid_size_units);
1213        let total_ask_size = qty_to_decimal(total_ask_size_units);
1214
1215        BookAnalytics {
1216            token_id: self.token_id.clone(),
1217            timestamp: self.timestamp,
1218            bid_count,
1219            ask_count,
1220            total_bid_size,
1221            total_ask_size,
1222            spread: self.spread(),
1223            spread_pct: self.spread_pct(),
1224            mid_price: self.mid_price(),
1225            volatility: self.calculate_volatility(),
1226        }
1227    }
1228
1229    /// Calculate price volatility (simplified)
1230    /// This is a placeholder - real volatility needs historical price data
1231    fn calculate_volatility(&self) -> Option<Decimal> {
1232        // This is a simplified volatility calculation
1233        // In a real implementation, you'd want to track price history over time
1234        // and calculate standard deviation of price changes
1235        None
1236    }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241    use super::*;
1242    use rust_decimal_macros::dec;
1243    use std::str::FromStr;
1244    use std::time::Duration; // Convenient macro for creating Decimal literals
1245
1246    #[test]
1247    fn test_order_book_creation() {
1248        // Test that we can create a new empty order book
1249        let book = OrderBook::new("test_token".to_string(), 10);
1250        assert_eq!(book.token_id, "test_token");
1251        assert_eq!(book.bids.len(), 0); // Should start empty
1252        assert_eq!(book.asks.len(), 0); // Should start empty
1253    }
1254
1255    #[test]
1256    fn test_set_tick_size_requires_exact_fixed_point_value() {
1257        let mut book = OrderBook::new("test_token".to_string(), 10);
1258
1259        book.set_tick_size(dec!(0.0001)).unwrap();
1260
1261        let err = book.set_tick_size(dec!(0.00005)).unwrap_err();
1262        assert!(err.to_string().contains("Invalid tick size"));
1263
1264        let err = book.set_tick_size(Decimal::ZERO).unwrap_err();
1265        assert!(err.to_string().contains("Invalid tick size"));
1266    }
1267
1268    #[test]
1269    fn test_order_book_manager_routes_tokens_to_shards() {
1270        let shard_count = 4;
1271        let first_token = "test_token_0";
1272        let first_shard = shard_index(first_token, shard_count);
1273        let second_token = (1..100)
1274            .map(|idx| format!("test_token_{idx}"))
1275            .find(|token| shard_index(token, shard_count) != first_shard)
1276            .expect("test tokens should cover multiple shards");
1277
1278        let manager = OrderBookManager::with_shard_count(10, shard_count);
1279        manager.get_or_create_book(first_token).unwrap();
1280        manager.get_or_create_book(&second_token).unwrap();
1281
1282        manager
1283            .apply_delta(OrderDelta {
1284                token_id: first_token.to_string(),
1285                timestamp: Utc::now(),
1286                side: Side::BUY,
1287                price: dec!(0.50),
1288                size: dec!(100),
1289                sequence: 1,
1290            })
1291            .unwrap();
1292        manager
1293            .apply_delta(OrderDelta {
1294                token_id: second_token.clone(),
1295                timestamp: Utc::now(),
1296                side: Side::SELL,
1297                price: dec!(0.60),
1298                size: dec!(200),
1299                sequence: 1,
1300            })
1301            .unwrap();
1302
1303        let first_book = manager.get_book(first_token).unwrap();
1304        let second_book = manager.get_book(&second_token).unwrap();
1305
1306        assert_eq!(first_book.bids.len(), 1);
1307        assert_eq!(first_book.asks.len(), 0);
1308        assert_eq!(second_book.bids.len(), 0);
1309        assert_eq!(second_book.asks.len(), 1);
1310        assert_eq!(manager.get_all_books().unwrap().len(), 2);
1311    }
1312
1313    #[test]
1314    fn test_apply_delta() {
1315        // Test that we can apply order book updates
1316        let mut book = OrderBook::new("test_token".to_string(), 10);
1317
1318        // Create a buy order at $0.50 for 100 tokens
1319        let delta = OrderDelta {
1320            token_id: "test_token".to_string(),
1321            timestamp: Utc::now(),
1322            side: Side::BUY,
1323            price: dec!(0.5),
1324            size: dec!(100),
1325            sequence: 1,
1326        };
1327
1328        book.apply_delta(delta).unwrap();
1329        assert_eq!(book.sequence, 1); // Sequence should update
1330        assert_eq!(book.last_delta_sequence, 1);
1331        assert_eq!(book.last_snapshot_timestamp_ms, 0);
1332        assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); // Should be our bid
1333        assert_eq!(book.best_bid().unwrap().size, dec!(100)); // Should be our size
1334    }
1335
1336    #[test]
1337    fn test_sorted_side_ordering_and_removal() {
1338        let mut book = OrderBook::new("test_token".to_string(), 10);
1339
1340        for (sequence, price) in [(1, dec!(0.73)), (2, dec!(0.75)), (3, dec!(0.74))] {
1341            book.apply_delta(OrderDelta {
1342                token_id: "test_token".to_string(),
1343                timestamp: Utc::now(),
1344                side: Side::BUY,
1345                price,
1346                size: dec!(100),
1347                sequence,
1348            })
1349            .unwrap();
1350        }
1351
1352        for (sequence, price) in [(4, dec!(0.78)), (5, dec!(0.76)), (6, dec!(0.77))] {
1353            book.apply_delta(OrderDelta {
1354                token_id: "test_token".to_string(),
1355                timestamp: Utc::now(),
1356                side: Side::SELL,
1357                price,
1358                size: dec!(100),
1359                sequence,
1360            })
1361            .unwrap();
1362        }
1363
1364        let bids: Vec<_> = book
1365            .bids(Some(3))
1366            .into_iter()
1367            .map(|level| level.price)
1368            .collect();
1369        let asks: Vec<_> = book
1370            .asks(Some(3))
1371            .into_iter()
1372            .map(|level| level.price)
1373            .collect();
1374        assert_eq!(bids, vec![dec!(0.75), dec!(0.74), dec!(0.73)]);
1375        assert_eq!(asks, vec![dec!(0.76), dec!(0.77), dec!(0.78)]);
1376
1377        book.apply_delta(OrderDelta {
1378            token_id: "test_token".to_string(),
1379            timestamp: Utc::now(),
1380            side: Side::BUY,
1381            price: dec!(0.75),
1382            size: Decimal::ZERO,
1383            sequence: 7,
1384        })
1385        .unwrap();
1386        book.apply_delta(OrderDelta {
1387            token_id: "test_token".to_string(),
1388            timestamp: Utc::now(),
1389            side: Side::SELL,
1390            price: dec!(0.76),
1391            size: Decimal::ZERO,
1392            sequence: 8,
1393        })
1394        .unwrap();
1395
1396        assert_eq!(book.best_bid().unwrap().price, dec!(0.74));
1397        assert_eq!(book.best_ask().unwrap().price, dec!(0.77));
1398    }
1399
1400    #[test]
1401    fn test_book_update_replaces_snapshot_and_uses_millis_timestamp() {
1402        let mut book = OrderBook::new("test_token".to_string(), 10);
1403        let timestamp = 1_757_908_892_351;
1404
1405        book.apply_book_update(&BookUpdate {
1406            asset_id: "test_token".to_string(),
1407            market: "0xabc".to_string(),
1408            timestamp,
1409            bids: vec![
1410                OrderSummary {
1411                    price: dec!(0.48),
1412                    size: dec!(10),
1413                },
1414                OrderSummary {
1415                    price: dec!(0.49),
1416                    size: dec!(20),
1417                },
1418            ],
1419            asks: vec![
1420                OrderSummary {
1421                    price: dec!(0.52),
1422                    size: dec!(30),
1423                },
1424                OrderSummary {
1425                    price: dec!(0.53),
1426                    size: dec!(40),
1427                },
1428            ],
1429            hash: None,
1430        })
1431        .unwrap();
1432
1433        book.apply_book_update(&BookUpdate {
1434            asset_id: "test_token".to_string(),
1435            market: "0xabc".to_string(),
1436            timestamp: timestamp + 1,
1437            bids: vec![OrderSummary {
1438                price: dec!(0.49),
1439                size: dec!(25),
1440            }],
1441            asks: vec![OrderSummary {
1442                price: dec!(0.53),
1443                size: dec!(45),
1444            }],
1445            hash: None,
1446        })
1447        .unwrap();
1448
1449        assert_eq!(book.timestamp.timestamp_millis(), timestamp as i64 + 1);
1450        assert_eq!(book.bids(None).len(), 1);
1451        assert_eq!(book.asks(None).len(), 1);
1452        assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1453        assert_eq!(book.best_bid().unwrap().size, dec!(25));
1454        assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1455        assert_eq!(book.best_ask().unwrap().size, dec!(45));
1456    }
1457
1458    #[test]
1459    fn test_ws_snapshot_timestamp_does_not_block_delta_sequence() {
1460        let mut book = OrderBook::new("test_token".to_string(), 10);
1461        let snapshot_timestamp_ms = 1_757_908_892_351;
1462        let levels = [
1463            ParsedBookLevel {
1464                side: Side::BUY,
1465                price_ticks: 5_000,
1466                size_units: 100_000,
1467            },
1468            ParsedBookLevel {
1469                side: Side::SELL,
1470                price_ticks: 6_000,
1471                size_units: 100_000,
1472            },
1473        ];
1474
1475        assert!(book
1476            .apply_ws_book_snapshot_fast("test_token", snapshot_timestamp_ms, None, &levels)
1477            .unwrap());
1478
1479        assert_eq!(book.sequence, 0);
1480        assert_eq!(book.last_delta_sequence, 0);
1481        assert_eq!(book.last_snapshot_timestamp_ms, snapshot_timestamp_ms);
1482
1483        book.apply_delta(OrderDelta {
1484            token_id: "test_token".to_string(),
1485            timestamp: Utc::now(),
1486            side: Side::BUY,
1487            price: dec!(0.51),
1488            size: dec!(11),
1489            sequence: 1,
1490        })
1491        .unwrap();
1492
1493        assert_eq!(book.sequence, 1);
1494        assert_eq!(book.last_delta_sequence, 1);
1495        assert_eq!(book.last_snapshot_timestamp_ms, snapshot_timestamp_ms);
1496        assert_eq!(book.best_bid().unwrap().price, dec!(0.51));
1497    }
1498
1499    #[test]
1500    fn test_delta_sequence_does_not_block_snapshot_timestamp() {
1501        let mut book = OrderBook::new("test_token".to_string(), 10);
1502
1503        book.apply_delta(OrderDelta {
1504            token_id: "test_token".to_string(),
1505            timestamp: Utc::now(),
1506            side: Side::BUY,
1507            price: dec!(0.40),
1508            size: dec!(10),
1509            sequence: 10_000,
1510        })
1511        .unwrap();
1512
1513        book.apply_book_update(&BookUpdate {
1514            asset_id: "test_token".to_string(),
1515            market: "0xabc".to_string(),
1516            timestamp: 1_000,
1517            bids: vec![OrderSummary {
1518                price: dec!(0.50),
1519                size: dec!(20),
1520            }],
1521            asks: vec![OrderSummary {
1522                price: dec!(0.60),
1523                size: dec!(30),
1524            }],
1525            hash: None,
1526        })
1527        .unwrap();
1528
1529        assert_eq!(book.sequence, 10_000);
1530        assert_eq!(book.last_delta_sequence, 10_000);
1531        assert_eq!(book.last_snapshot_timestamp_ms, 1_000);
1532        assert_eq!(book.best_bid().unwrap().price, dec!(0.50));
1533        assert_eq!(book.best_ask().unwrap().price, dec!(0.60));
1534
1535        let snapshot = book.snapshot();
1536        assert_eq!(snapshot.sequence, 10_000);
1537        assert_eq!(snapshot.last_delta_sequence, 10_000);
1538        assert_eq!(snapshot.last_snapshot_timestamp_ms, 1_000);
1539    }
1540
1541    #[test]
1542    fn test_book_update_allows_same_timestamp_with_different_hash() {
1543        let mut book = OrderBook::new("test_token".to_string(), 10);
1544        let timestamp = 1_757_908_892_351;
1545
1546        book.apply_book_update(&BookUpdate {
1547            asset_id: "test_token".to_string(),
1548            market: "0xabc".to_string(),
1549            timestamp,
1550            bids: vec![OrderSummary {
1551                price: dec!(0.48),
1552                size: dec!(10),
1553            }],
1554            asks: vec![OrderSummary {
1555                price: dec!(0.52),
1556                size: dec!(20),
1557            }],
1558            hash: Some("hash_a".to_string()),
1559        })
1560        .unwrap();
1561
1562        book.apply_book_update(&BookUpdate {
1563            asset_id: "test_token".to_string(),
1564            market: "0xabc".to_string(),
1565            timestamp,
1566            bids: vec![OrderSummary {
1567                price: dec!(0.49),
1568                size: dec!(30),
1569            }],
1570            asks: vec![OrderSummary {
1571                price: dec!(0.53),
1572                size: dec!(40),
1573            }],
1574            hash: Some("hash_b".to_string()),
1575        })
1576        .unwrap();
1577
1578        assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1579        assert_eq!(book.best_bid().unwrap().size, dec!(30));
1580        assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1581        assert_eq!(book.best_ask().unwrap().size, dec!(40));
1582
1583        book.apply_book_update(&BookUpdate {
1584            asset_id: "test_token".to_string(),
1585            market: "0xabc".to_string(),
1586            timestamp,
1587            bids: vec![OrderSummary {
1588                price: dec!(0.50),
1589                size: dec!(50),
1590            }],
1591            asks: vec![OrderSummary {
1592                price: dec!(0.54),
1593                size: dec!(60),
1594            }],
1595            hash: Some("hash_b".to_string()),
1596        })
1597        .unwrap();
1598
1599        assert_eq!(book.best_bid().unwrap().price, dec!(0.49));
1600        assert_eq!(book.best_ask().unwrap().price, dec!(0.53));
1601    }
1602
1603    #[test]
1604    fn test_book_update_rejects_same_timestamp_without_hash_tie_breaker() {
1605        let mut book = OrderBook::new("test_token".to_string(), 10);
1606        let timestamp = 1_757_908_892_351;
1607
1608        book.apply_book_update(&BookUpdate {
1609            asset_id: "test_token".to_string(),
1610            market: "0xabc".to_string(),
1611            timestamp,
1612            bids: vec![OrderSummary {
1613                price: dec!(0.48),
1614                size: dec!(10),
1615            }],
1616            asks: vec![OrderSummary {
1617                price: dec!(0.52),
1618                size: dec!(20),
1619            }],
1620            hash: None,
1621        })
1622        .unwrap();
1623
1624        book.apply_book_update(&BookUpdate {
1625            asset_id: "test_token".to_string(),
1626            market: "0xabc".to_string(),
1627            timestamp,
1628            bids: vec![OrderSummary {
1629                price: dec!(0.49),
1630                size: dec!(30),
1631            }],
1632            asks: vec![OrderSummary {
1633                price: dec!(0.53),
1634                size: dec!(40),
1635            }],
1636            hash: None,
1637        })
1638        .unwrap();
1639
1640        assert_eq!(book.best_bid().unwrap().price, dec!(0.48));
1641        assert_eq!(book.best_ask().unwrap().price, dec!(0.52));
1642    }
1643
1644    #[test]
1645    fn test_book_update_error_keeps_existing_snapshot() {
1646        let mut book = OrderBook::new("test_token".to_string(), 10);
1647        book.set_tick_size_ticks(100);
1648
1649        book.apply_book_update(&BookUpdate {
1650            asset_id: "test_token".to_string(),
1651            market: "0xabc".to_string(),
1652            timestamp: 100,
1653            bids: vec![OrderSummary {
1654                price: dec!(0.50),
1655                size: dec!(10),
1656            }],
1657            asks: vec![OrderSummary {
1658                price: dec!(0.60),
1659                size: dec!(20),
1660            }],
1661            hash: None,
1662        })
1663        .unwrap();
1664
1665        let err = book
1666            .apply_book_update(&BookUpdate {
1667                asset_id: "test_token".to_string(),
1668                market: "0xabc".to_string(),
1669                timestamp: 101,
1670                bids: vec![
1671                    OrderSummary {
1672                        price: dec!(0.51),
1673                        size: dec!(11),
1674                    },
1675                    OrderSummary {
1676                        price: dec!(0.515),
1677                        size: dec!(12),
1678                    },
1679                ],
1680                asks: vec![OrderSummary {
1681                    price: dec!(0.61),
1682                    size: dec!(21),
1683                }],
1684                hash: None,
1685            })
1686            .unwrap_err();
1687
1688        assert!(err.to_string().contains("Price not aligned"));
1689        assert_eq!(book.sequence, 0);
1690        assert_eq!(book.last_delta_sequence, 0);
1691        assert_eq!(book.last_snapshot_timestamp_ms, 100);
1692        assert_eq!(book.bids(None).len(), 1);
1693        assert_eq!(book.asks(None).len(), 1);
1694        assert_eq!(book.best_bid().unwrap().price, dec!(0.50));
1695        assert_eq!(book.best_bid().unwrap().size, dec!(10));
1696        assert_eq!(book.best_ask().unwrap().price, dec!(0.60));
1697        assert_eq!(book.best_ask().unwrap().size, dec!(20));
1698    }
1699
1700    #[test]
1701    fn test_book_update_depth_keeps_best_prices_independent_of_payload_order() {
1702        let mut book = OrderBook::new("test_token".to_string(), 2);
1703
1704        book.apply_book_update(&BookUpdate {
1705            asset_id: "test_token".to_string(),
1706            market: "0xabc".to_string(),
1707            timestamp: 1_757_908_892_351,
1708            bids: vec![
1709                OrderSummary {
1710                    price: dec!(0.49),
1711                    size: dec!(10),
1712                },
1713                OrderSummary {
1714                    price: dec!(0.50),
1715                    size: dec!(20),
1716                },
1717                OrderSummary {
1718                    price: dec!(0.48),
1719                    size: dec!(30),
1720                },
1721            ],
1722            asks: vec![
1723                OrderSummary {
1724                    price: dec!(0.52),
1725                    size: dec!(40),
1726                },
1727                OrderSummary {
1728                    price: dec!(0.54),
1729                    size: dec!(50),
1730                },
1731                OrderSummary {
1732                    price: dec!(0.53),
1733                    size: dec!(60),
1734                },
1735            ],
1736            hash: None,
1737        })
1738        .unwrap();
1739
1740        let bids = book.bids(None);
1741        assert_eq!(bids.len(), 2);
1742        assert_eq!(bids[0].price, dec!(0.50));
1743        assert_eq!(bids[1].price, dec!(0.49));
1744
1745        let asks = book.asks(None);
1746        assert_eq!(asks.len(), 2);
1747        assert_eq!(asks[0].price, dec!(0.52));
1748        assert_eq!(asks[1].price, dec!(0.53));
1749    }
1750
1751    #[test]
1752    fn test_spread_calculation() {
1753        // Test that we can calculate the spread between bid and ask
1754        let mut book = OrderBook::new("test_token".to_string(), 10);
1755
1756        // Add a bid at $0.50
1757        book.apply_delta(OrderDelta {
1758            token_id: "test_token".to_string(),
1759            timestamp: Utc::now(),
1760            side: Side::BUY,
1761            price: dec!(0.5),
1762            size: dec!(100),
1763            sequence: 1,
1764        })
1765        .unwrap();
1766
1767        // Add an ask at $0.52
1768        book.apply_delta(OrderDelta {
1769            token_id: "test_token".to_string(),
1770            timestamp: Utc::now(),
1771            side: Side::SELL,
1772            price: dec!(0.52),
1773            size: dec!(100),
1774            sequence: 2,
1775        })
1776        .unwrap();
1777
1778        let spread = book.spread().unwrap();
1779        assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02
1780    }
1781
1782    #[test]
1783    fn test_market_impact() {
1784        // Test market impact calculation for a large order
1785        let mut book = OrderBook::new("test_token".to_string(), 10);
1786
1787        // Add multiple ask levels (people selling at different prices)
1788        // $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens
1789        for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
1790            book.apply_delta(OrderDelta {
1791                token_id: "test_token".to_string(),
1792                timestamp: Utc::now(),
1793                side: Side::SELL,
1794                price: *price,
1795                size: dec!(100),
1796                sequence: i as u64 + 1,
1797            })
1798            .unwrap();
1799        }
1800
1801        // Try to buy 150 tokens (will need to hit multiple price levels)
1802        let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap();
1803        assert!(impact.average_price > dec!(0.50)); // Should be worse than best price
1804        assert!(impact.average_price < dec!(0.51)); // But not as bad as second level
1805    }
1806
1807    #[test]
1808    fn test_apply_bid_delta_legacy() {
1809        let mut book = OrderBook::new("test_token".to_string(), 10);
1810
1811        // Test adding a bid
1812        book.apply_bid_delta(
1813            Decimal::from_str("0.75").unwrap(),
1814            Decimal::from_str("100.0").unwrap(),
1815        );
1816
1817        let best_bid = book.best_bid();
1818        assert!(best_bid.is_some());
1819        let bid = best_bid.unwrap();
1820        assert_eq!(bid.price, Decimal::from_str("0.75").unwrap());
1821        assert_eq!(bid.size, Decimal::from_str("100.0").unwrap());
1822
1823        // Test updating the bid
1824        book.apply_bid_delta(
1825            Decimal::from_str("0.75").unwrap(),
1826            Decimal::from_str("150.0").unwrap(),
1827        );
1828        let updated_bid = book.best_bid().unwrap();
1829        assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap());
1830
1831        // Test removing the bid
1832        book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO);
1833        assert!(book.best_bid().is_none());
1834    }
1835
1836    #[test]
1837    fn test_apply_ask_delta_legacy() {
1838        let mut book = OrderBook::new("test_token".to_string(), 10);
1839
1840        // Test adding an ask
1841        book.apply_ask_delta(
1842            Decimal::from_str("0.76").unwrap(),
1843            Decimal::from_str("50.0").unwrap(),
1844        );
1845
1846        let best_ask = book.best_ask();
1847        assert!(best_ask.is_some());
1848        let ask = best_ask.unwrap();
1849        assert_eq!(ask.price, Decimal::from_str("0.76").unwrap());
1850        assert_eq!(ask.size, Decimal::from_str("50.0").unwrap());
1851
1852        // Test updating the ask
1853        book.apply_ask_delta(
1854            Decimal::from_str("0.76").unwrap(),
1855            Decimal::from_str("75.0").unwrap(),
1856        );
1857        let updated_ask = book.best_ask().unwrap();
1858        assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap());
1859
1860        // Test removing the ask
1861        book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO);
1862        assert!(book.best_ask().is_none());
1863    }
1864
1865    #[test]
1866    fn test_liquidity_analysis() {
1867        let mut book = OrderBook::new("test_token".to_string(), 10);
1868
1869        // Build order book using legacy methods
1870        book.apply_bid_delta(
1871            Decimal::from_str("0.75").unwrap(),
1872            Decimal::from_str("100.0").unwrap(),
1873        );
1874        book.apply_bid_delta(
1875            Decimal::from_str("0.74").unwrap(),
1876            Decimal::from_str("50.0").unwrap(),
1877        );
1878        book.apply_ask_delta(
1879            Decimal::from_str("0.76").unwrap(),
1880            Decimal::from_str("80.0").unwrap(),
1881        );
1882        book.apply_ask_delta(
1883            Decimal::from_str("0.77").unwrap(),
1884            Decimal::from_str("120.0").unwrap(),
1885        );
1886
1887        // Test liquidity at specific price - when buying, we look at ask liquidity
1888        let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY);
1889        assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap());
1890
1891        // Test liquidity at specific price - when selling, we look at bid liquidity
1892        let sell_liquidity =
1893            book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
1894        assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap());
1895
1896        // Test liquidity in range - when buying, we look at ask liquidity in range
1897        let buy_range_liquidity = book.liquidity_in_range(
1898            Decimal::from_str("0.74").unwrap(),
1899            Decimal::from_str("0.77").unwrap(),
1900            Side::BUY,
1901        );
1902        // Should include ask liquidity: 80 (0.76 ask) + 120 (0.77 ask) = 200
1903        assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap());
1904
1905        // Test liquidity in range - when selling, we look at bid liquidity in range
1906        let sell_range_liquidity = book.liquidity_in_range(
1907            Decimal::from_str("0.74").unwrap(),
1908            Decimal::from_str("0.77").unwrap(),
1909            Side::SELL,
1910        );
1911        // Should include bid liquidity: 50 (0.74 bid) + 100 (0.75 bid) = 150
1912        assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap());
1913    }
1914
1915    #[test]
1916    fn test_book_validation() {
1917        let mut book = OrderBook::new("test_token".to_string(), 10);
1918
1919        // Empty book should be valid
1920        assert!(book.is_valid());
1921
1922        // Add normal levels
1923        book.apply_bid_delta(
1924            Decimal::from_str("0.75").unwrap(),
1925            Decimal::from_str("100.0").unwrap(),
1926        );
1927        book.apply_ask_delta(
1928            Decimal::from_str("0.76").unwrap(),
1929            Decimal::from_str("80.0").unwrap(),
1930        );
1931        assert!(book.is_valid());
1932
1933        // Create crossed book (invalid) - bid higher than ask
1934        book.apply_bid_delta(
1935            Decimal::from_str("0.77").unwrap(),
1936            Decimal::from_str("50.0").unwrap(),
1937        );
1938        assert!(!book.is_valid());
1939    }
1940
1941    #[test]
1942    fn test_book_staleness() {
1943        let mut book = OrderBook::new("test_token".to_string(), 10);
1944
1945        // Fresh book should not be stale
1946        assert!(!book.is_stale(Duration::from_secs(60))); // 60 second threshold
1947
1948        // Add some data
1949        book.apply_bid_delta(
1950            Decimal::from_str("0.75").unwrap(),
1951            Decimal::from_str("100.0").unwrap(),
1952        );
1953        assert!(!book.is_stale(Duration::from_secs(60)));
1954
1955        // Note: We can't easily test actual staleness without manipulating time,
1956        // but we can test the method exists and works with fresh data
1957    }
1958
1959    #[test]
1960    fn test_depth_management() {
1961        let mut book = OrderBook::new("test_token".to_string(), 3); // Only 3 levels
1962
1963        // Add multiple levels
1964        book.apply_bid_delta(
1965            Decimal::from_str("0.75").unwrap(),
1966            Decimal::from_str("100.0").unwrap(),
1967        );
1968        book.apply_bid_delta(
1969            Decimal::from_str("0.74").unwrap(),
1970            Decimal::from_str("50.0").unwrap(),
1971        );
1972        book.apply_bid_delta(
1973            Decimal::from_str("0.73").unwrap(),
1974            Decimal::from_str("20.0").unwrap(),
1975        );
1976
1977        book.apply_ask_delta(
1978            Decimal::from_str("0.76").unwrap(),
1979            Decimal::from_str("80.0").unwrap(),
1980        );
1981        book.apply_ask_delta(
1982            Decimal::from_str("0.77").unwrap(),
1983            Decimal::from_str("40.0").unwrap(),
1984        );
1985        book.apply_ask_delta(
1986            Decimal::from_str("0.78").unwrap(),
1987            Decimal::from_str("30.0").unwrap(),
1988        );
1989
1990        // Should have levels on each side
1991        let bids = book.bids(Some(3));
1992        let asks = book.asks(Some(3));
1993
1994        assert!(bids.len() <= 3);
1995        assert!(asks.len() <= 3);
1996
1997        // Best levels should be there
1998        assert_eq!(
1999            book.best_bid().unwrap().price,
2000            Decimal::from_str("0.75").unwrap()
2001        );
2002        assert_eq!(
2003            book.best_ask().unwrap().price,
2004            Decimal::from_str("0.76").unwrap()
2005        );
2006    }
2007
2008    #[test]
2009    fn test_fast_operations() {
2010        let mut book = OrderBook::new("test_token".to_string(), 10);
2011
2012        // Test using legacy methods which call fast operations internally
2013        book.apply_bid_delta(
2014            Decimal::from_str("0.75").unwrap(),
2015            Decimal::from_str("100.0").unwrap(),
2016        );
2017        book.apply_ask_delta(
2018            Decimal::from_str("0.76").unwrap(),
2019            Decimal::from_str("80.0").unwrap(),
2020        );
2021
2022        let best_bid_fast = book.best_bid_fast();
2023        let best_ask_fast = book.best_ask_fast();
2024
2025        assert!(best_bid_fast.is_some());
2026        assert!(best_ask_fast.is_some());
2027
2028        // Test fast spread and mid price
2029        let spread_fast = book.spread_fast();
2030        let mid_fast = book.mid_price_fast();
2031
2032        assert!(spread_fast.is_some()); // Should have a spread
2033        assert!(mid_fast.is_some()); // Should have a mid price
2034    }
2035}