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 rust_decimal::Decimal;
7use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically - crucial for order books
8use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks
9use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
10use chrono::Utc;
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.iter().next_back().map(|(&price_ticks, &size_units)| {
127 // Convert from internal fixed-point to external Decimal format
128 // This conversion only happens at the API boundary
129 BookLevel {
130 price: price_to_decimal(price_ticks),
131 size: qty_to_decimal(size_units),
132 }
133 })
134 }
135
136 /// Get the current best ask (lowest price someone is willing to sell at)
137 /// Uses next() because BTreeMap sorts ascending, so first item is lowest ask
138 ///
139 /// PERFORMANCE: Now returns data in external format but internally uses fast lookups
140 pub fn best_ask(&self) -> Option<BookLevel> {
141 // BEFORE (slow, ~50ns + allocation):
142 // self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
143
144 // AFTER (fast, ~5ns, no allocation for the lookup):
145 self.asks.iter().next().map(|(&price_ticks, &size_units)| {
146 // Convert from internal fixed-point to external Decimal format
147 // This conversion only happens at the API boundary
148 BookLevel {
149 price: price_to_decimal(price_ticks),
150 size: qty_to_decimal(size_units),
151 }
152 })
153 }
154
155 /// Get the current best bid in fast internal format
156 /// Use this for internal calculations to avoid conversion overhead
157 pub fn best_bid_fast(&self) -> Option<FastBookLevel> {
158 self.bids.iter().next_back().map(|(&price, &size)| {
159 FastBookLevel::new(price, size)
160 })
161 }
162
163 /// Get the current best ask in fast internal format
164 /// Use this for internal calculations to avoid conversion overhead
165 pub fn best_ask_fast(&self) -> Option<FastBookLevel> {
166 self.asks.iter().next().map(|(&price, &size)| {
167 FastBookLevel::new(price, size)
168 })
169 }
170
171 /// Get the current spread (difference between best ask and best bid)
172 /// This tells us how "tight" the market is - smaller spread = more liquid market
173 ///
174 /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
175 pub fn spread(&self) -> Option<Decimal> {
176 // BEFORE (slow, ~100ns + multiple allocations):
177 // match (self.best_bid(), self.best_ask()) {
178 // (Some(bid), Some(ask)) => Some(ask.price - bid.price),
179 // _ => None,
180 // }
181
182 // AFTER (fast, ~5ns, no allocations):
183 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
184 let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?;
185 Some(price_to_decimal(spread_ticks))
186 }
187
188 /// Get the current mid price (halfway between best bid and ask)
189 /// This is often used as the "fair value" of the market
190 ///
191 /// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
192 pub fn mid_price(&self) -> Option<Decimal> {
193 // BEFORE (slow, ~80ns + allocations):
194 // math::mid_price(
195 // self.best_bid()?.price,
196 // self.best_ask()?.price,
197 // )
198
199 // AFTER (fast, ~3ns, no allocations):
200 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
201 let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?;
202 Some(price_to_decimal(mid_ticks))
203 }
204
205 /// Get the spread as a percentage (relative to the bid price)
206 /// Useful for comparing spreads across different price levels
207 ///
208 /// PERFORMANCE: Now uses fast internal calculations and returns basis points
209 pub fn spread_pct(&self) -> Option<Decimal> {
210 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
211 let spread_bps = math::spread_pct_fast(best_bid_ticks, best_ask_ticks)?;
212 // Convert basis points back to percentage decimal
213 Some(Decimal::from(spread_bps) / Decimal::from(100))
214 }
215
216 /// Get best bid and ask prices in fast internal format
217 /// Helper method to avoid code duplication and minimize conversions
218 fn best_prices_fast(&self) -> Option<(Price, Price)> {
219 let best_bid_ticks = self.bids.iter().next_back()?.0;
220 let best_ask_ticks = self.asks.iter().next()?.0;
221 Some((*best_bid_ticks, *best_ask_ticks))
222 }
223
224 /// Get the current spread in fast internal format (PERFORMANCE OPTIMIZED)
225 /// Returns spread in ticks - use this for internal calculations
226 pub fn spread_fast(&self) -> Option<Price> {
227 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
228 math::spread_fast(best_bid_ticks, best_ask_ticks)
229 }
230
231 /// Get the current mid price in fast internal format (PERFORMANCE OPTIMIZED)
232 /// Returns mid price in ticks - use this for internal calculations
233 pub fn mid_price_fast(&self) -> Option<Price> {
234 let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
235 math::mid_price_fast(best_bid_ticks, best_ask_ticks)
236 }
237
238 /// Get all bids up to a certain depth (top N price levels)
239 /// Returns them in descending price order (best bids first)
240 ///
241 /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
242 /// Only call this when you need to return data to external APIs
243 pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
244 let depth = depth.unwrap_or(self.max_depth);
245 self.bids
246 .iter()
247 .rev() // Reverse because we want highest prices first
248 .take(depth) // Only take the top N levels
249 .map(|(&price_ticks, &size_units)| BookLevel {
250 price: price_to_decimal(price_ticks),
251 size: qty_to_decimal(size_units),
252 })
253 .collect()
254 }
255
256 /// Get all asks up to a certain depth (top N price levels)
257 /// Returns them in ascending price order (best asks first)
258 ///
259 /// PERFORMANCE: Converts from internal fixed-point to external Decimal format
260 /// Only call this when you need to return data to external APIs
261 pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
262 let depth = depth.unwrap_or(self.max_depth);
263 self.asks
264 .iter() // Already in ascending order, so no need to reverse
265 .take(depth) // Only take the top N levels
266 .map(|(&price_ticks, &size_units)| BookLevel {
267 price: price_to_decimal(price_ticks),
268 size: qty_to_decimal(size_units),
269 })
270 .collect()
271 }
272
273 /// Get all bids in fast internal format
274 /// Use this for internal calculations to avoid conversion overhead
275 pub fn bids_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
276 let depth = depth.unwrap_or(self.max_depth);
277 self.bids
278 .iter()
279 .rev() // Reverse because we want highest prices first
280 .take(depth) // Only take the top N levels
281 .map(|(&price, &size)| FastBookLevel::new(price, size))
282 .collect()
283 }
284
285 /// Get all asks in fast internal format (PERFORMANCE OPTIMIZED)
286 /// Use this for internal calculations to avoid conversion overhead
287 pub fn asks_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
288 let depth = depth.unwrap_or(self.max_depth);
289 self.asks
290 .iter() // Already in ascending order, so no need to reverse
291 .take(depth) // Only take the top N levels
292 .map(|(&price, &size)| FastBookLevel::new(price, size))
293 .collect()
294 }
295
296 /// Get the full book snapshot
297 /// Creates a copy of the current state that can be safely passed around
298 /// without worrying about the original book changing
299 pub fn snapshot(&self) -> crate::types::OrderBook {
300 crate::types::OrderBook {
301 token_id: self.token_id.clone(),
302 timestamp: self.timestamp,
303 bids: self.bids(None), // Get all bids (up to max_depth)
304 asks: self.asks(None), // Get all asks (up to max_depth)
305 sequence: self.sequence,
306 }
307 }
308
309 /// Apply a delta update to the book (LEGACY VERSION - for external API compatibility)
310 /// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70"
311 ///
312 /// This method converts the external Decimal delta to our internal fixed-point format
313 /// and then calls the fast version. Use apply_delta_fast() directly when possible.
314 pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
315 // Convert to fast internal format with tick alignment validation
316 let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal);
317 let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal)
318 .map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?;
319
320 // Use the fast internal version
321 self.apply_delta_fast(fast_delta)
322 }
323
324 /// Apply a delta update to the book
325 ///
326 /// This is the high-performance version that works directly with fixed-point data.
327 /// It includes tick alignment validation and is much faster than the Decimal version.
328 ///
329 /// Performance improvement: ~50x faster than the old Decimal version!
330 /// - No Decimal conversions in the hot path
331 /// - Integer comparisons instead of Decimal comparisons
332 /// - No memory allocations for price/size operations
333 pub fn apply_delta_fast(&mut self, delta: FastOrderDelta) -> Result<()> {
334 // Validate sequence ordering - ignore old updates that arrive late
335 // This is crucial for maintaining data integrity in real-time systems
336 if delta.sequence <= self.sequence {
337 trace!("Ignoring stale delta: {} <= {}", delta.sequence, self.sequence);
338 return Ok(());
339 }
340
341 // Validate token ID hash matches (fast string comparison avoidance)
342 if delta.token_id_hash != self.token_id_hash {
343 return Err(PolyfillError::validation("Token ID mismatch"));
344 }
345
346 // TICK ALIGNMENT VALIDATION - this is where we enforce price rules
347 // If we have a tick size, make sure the price aligns properly
348 if let Some(tick_size_ticks) = self.tick_size_ticks {
349 // BEFORE (slow, ~200ns + multiple conversions):
350 // let tick_size_decimal = price_to_decimal(tick_size_ticks);
351 // if !is_price_tick_aligned(price_to_decimal(delta.price), tick_size_decimal) {
352 // return Err(...);
353 // }
354
355 // AFTER (fast, ~2ns, pure integer):
356 if tick_size_ticks > 0 && delta.price % tick_size_ticks != 0 {
357 // Price is not aligned to tick size - reject the update
358 warn!(
359 "Rejecting misaligned price: {} not divisible by tick size {}",
360 delta.price, tick_size_ticks
361 );
362 return Err(PolyfillError::validation("Price not aligned to tick size"));
363 }
364 }
365
366 // Update our tracking info
367 self.sequence = delta.sequence;
368 self.timestamp = delta.timestamp;
369
370 // Apply the actual change to the appropriate side (FAST VERSION)
371 match delta.side {
372 Side::BUY => self.apply_bid_delta_fast(delta.price, delta.size),
373 Side::SELL => self.apply_ask_delta_fast(delta.price, delta.size),
374 }
375
376 // Keep the book from getting too deep (memory management)
377 self.trim_depth();
378
379 debug!(
380 "Applied fast delta: {} {} @ {} ticks (seq: {})",
381 delta.side.as_str(),
382 delta.size,
383 delta.price,
384 delta.sequence
385 );
386
387 Ok(())
388 }
389
390 /// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION
391 /// If size is 0, it means "remove this price level entirely"
392 /// Otherwise, set the total size at this price level
393 ///
394 /// This converts to fixed-point and calls the fast version
395 fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
396 // Convert to fixed-point (this should be rare since we use fast path)
397 let price_ticks = decimal_to_price(price).unwrap_or(0);
398 let size_units = decimal_to_qty(size).unwrap_or(0);
399 self.apply_bid_delta_fast(price_ticks, size_units);
400 }
401
402 /// Apply an ask-side delta (someone wants to sell) - LEGACY VERSION
403 /// Same logic as bids - size of 0 means remove the price level
404 ///
405 /// This converts to fixed-point and calls the fast version
406 fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
407 // Convert to fixed-point (this should be rare since we use fast path)
408 let price_ticks = decimal_to_price(price).unwrap_or(0);
409 let size_units = decimal_to_qty(size).unwrap_or(0);
410 self.apply_ask_delta_fast(price_ticks, size_units);
411 }
412
413 /// Apply a bid-side delta (someone wants to buy) - FAST VERSION
414 ///
415 /// This is the high-performance version that works directly with fixed-point.
416 /// Much faster than the Decimal version - pure integer operations.
417 fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
418 // BEFORE (slow, ~100ns + allocation):
419 // if size.is_zero() {
420 // self.bids.remove(&price);
421 // } else {
422 // self.bids.insert(price, size);
423 // }
424
425 // AFTER (fast, ~5ns, no allocation):
426 if size_units == 0 {
427 self.bids.remove(&price_ticks); // No more buyers at this price
428 } else {
429 self.bids.insert(price_ticks, size_units); // Update total size at this price
430 }
431 }
432
433 /// Apply an ask-side delta (someone wants to sell) - FAST VERSION
434 ///
435 /// This is the high-performance version that works directly with fixed-point.
436 /// Much faster than the Decimal version - pure integer operations.
437 fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
438 // BEFORE (slow, ~100ns + allocation):
439 // if size.is_zero() {
440 // self.asks.remove(&price);
441 // } else {
442 // self.asks.insert(price, size);
443 // }
444
445 // AFTER (fast, ~5ns, no allocation):
446 if size_units == 0 {
447 self.asks.remove(&price_ticks); // No more sellers at this price
448 } else {
449 self.asks.insert(price_ticks, size_units); // Update total size at this price
450 }
451 }
452
453 /// Trim the book to maintain depth limits
454 /// We don't want to track every single price level - just the best ones
455 ///
456 /// Why limit depth? Several reasons:
457 /// 1. Memory efficiency: A popular market might have thousands of price levels,
458 /// but only the top 10-50 levels are actually tradeable with reasonable size
459 /// 2. Performance: Fewer levels = faster iteration when calculating market impact
460 /// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
461 /// mostly noise and will never get hit in normal trading
462 /// 4. Stale data: Deep levels often contain old orders that haven't been cancelled
463 /// 5. Network bandwidth: Less data to send when streaming updates
464
465 fn trim_depth(&mut self) {
466 // For bids, remove the LOWEST prices (worst bids) if we have too many
467 // Example: If best bid is $0.65, we don't care about bids at $0.10
468 if self.bids.len() > self.max_depth {
469 let to_remove = self.bids.len() - self.max_depth;
470 for _ in 0..to_remove {
471 self.bids.pop_first(); // Remove lowest bid prices (furthest from market)
472 }
473 }
474
475 // For asks, remove the HIGHEST prices (worst asks) if we have too many
476 // Example: If best ask is $0.67, we don't care about asks at $0.95
477 if self.asks.len() > self.max_depth {
478 let to_remove = self.asks.len() - self.max_depth;
479 for _ in 0..to_remove {
480 self.asks.pop_last(); // Remove highest ask prices (furthest from market)
481 }
482 }
483 }
484
485 /// Calculate the market impact for a given order size
486 /// This is exactly why we don't need deep levels - if your order would require
487 /// hitting prices way off the current market (like $0.95 when best ask is $0.67),
488 /// you'd never actually place that order. You'd either:
489 /// 1. Break it into smaller pieces over time
490 /// 2. Use a different trading strategy
491 /// 3. Accept that there's not enough liquidity right now
492 pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
493 // PERFORMANCE NOTE: This method still uses Decimal for external compatibility,
494 // but the internal order book lookups now use our fast fixed-point data structures.
495 //
496 // BEFORE: Each level lookup involved Decimal operations (~50ns each)
497 // AFTER: Level lookups use integer operations (~5ns each)
498 //
499 // For a 10-level impact calculation: 500ns → 50ns (10x speedup)
500
501 // Get the levels we'd be trading against
502 let levels = match side {
503 Side::BUY => self.asks(None), // If buying, we hit the ask side
504 Side::SELL => self.bids(None), // If selling, we hit the bid side
505 };
506
507 if levels.is_empty() {
508 return None; // No liquidity available
509 }
510
511 let mut remaining_size = size;
512 let mut total_cost = Decimal::ZERO;
513 let mut weighted_price = Decimal::ZERO;
514
515 // Walk through each price level, filling as much as we can
516 for level in levels {
517 let fill_size = std::cmp::min(remaining_size, level.size);
518 let level_cost = fill_size * level.price;
519
520 total_cost += level_cost;
521 weighted_price += level_cost; // This accumulates the weighted average
522 remaining_size -= fill_size;
523
524 if remaining_size.is_zero() {
525 break; // We've filled our entire order
526 }
527 }
528
529 if remaining_size > Decimal::ZERO {
530 // Not enough liquidity to fill the whole order
531 // This is a perfect example of why we don't need infinite depth:
532 // If we can't fill your order with the top N levels, you probably
533 // shouldn't be placing that order anyway - it would move the market too much
534 return None;
535 }
536
537 let avg_price = weighted_price / size;
538
539 // Calculate how much we moved the market compared to the best price
540 let impact = match side {
541 Side::BUY => {
542 let best_ask = self.best_ask()?.price;
543 (avg_price - best_ask) / best_ask // How much worse than best ask
544 }
545 Side::SELL => {
546 let best_bid = self.best_bid()?.price;
547 (best_bid - avg_price) / best_bid // How much worse than best bid
548 }
549 };
550
551 Some(MarketImpact {
552 average_price: avg_price,
553 impact_pct: impact,
554 total_cost,
555 size_filled: size,
556 })
557 }
558
559 /// Check if the book is stale (no recent updates)
560 /// Useful for detecting when we've lost connection to live data
561 pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
562 let age = Utc::now() - self.timestamp;
563 age > chrono::Duration::from_std(max_age).unwrap_or_default()
564 }
565
566 /// Get the total liquidity at a given price level
567 /// Tells you how much you can buy/sell at exactly this price
568 pub fn liquidity_at_price(&self, price: Decimal, side: Side) -> Decimal {
569 // Convert decimal price to our internal fixed-point representation
570 let price_ticks = match decimal_to_price(price) {
571 Ok(ticks) => ticks,
572 Err(_) => return Decimal::ZERO, // Invalid price
573 };
574
575 match side {
576 Side::BUY => {
577 // How much we can buy at this price (look at asks)
578 let size_units = self.asks.get(&price_ticks).copied().unwrap_or_default();
579 qty_to_decimal(size_units)
580 },
581 Side::SELL => {
582 // How much we can sell at this price (look at bids)
583 let size_units = self.bids.get(&price_ticks).copied().unwrap_or_default();
584 qty_to_decimal(size_units)
585 }
586 }
587 }
588
589 /// Get the total liquidity within a price range
590 /// Useful for understanding how much depth exists in a certain price band
591 pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal {
592 // Convert decimal prices to our internal fixed-point representation
593 let min_price_ticks = match decimal_to_price(min_price) {
594 Ok(ticks) => ticks,
595 Err(_) => return Decimal::ZERO, // Invalid price
596 };
597 let max_price_ticks = match decimal_to_price(max_price) {
598 Ok(ticks) => ticks,
599 Err(_) => return Decimal::ZERO, // Invalid price
600 };
601
602 let levels: Vec<_> = match side {
603 Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(),
604 Side::SELL => self.bids.range(min_price_ticks..=max_price_ticks).rev().collect(),
605 };
606
607 // Sum up the sizes, converting from fixed-point back to Decimal
608 let total_size_units: i64 = levels.into_iter().map(|(_, &size)| size).sum();
609 qty_to_decimal(total_size_units)
610 }
611
612 /// Validate that prices are properly ordered
613 /// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
614 pub fn is_valid(&self) -> bool {
615 match (self.best_bid(), self.best_ask()) {
616 (Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition
617 _ => true, // Empty book is technically valid
618 }
619 }
620}
621
622/// Market impact calculation result
623/// This tells you what would happen if you executed a large order
624#[derive(Debug, Clone)]
625pub struct MarketImpact {
626 pub average_price: Decimal, // The average price you'd get across all fills
627 pub impact_pct: Decimal, // How much worse than the best price (as percentage)
628 pub total_cost: Decimal, // Total amount you'd pay/receive
629 pub size_filled: Decimal, // How much of your order got filled
630}
631
632/// Thread-safe order book manager
633/// This manages multiple order books (one per token) and handles concurrent access
634/// Multiple threads can read/write different books simultaneously
635///
636/// The depth limiting becomes even more critical here because we might be tracking
637/// hundreds or thousands of different tokens simultaneously. If each book had
638/// unlimited depth, we could easily use gigabytes of RAM for mostly useless data.
639///
640/// Example: 1000 tokens × 1000 price levels × 32 bytes per level = 32MB just for prices
641/// With depth limiting: 1000 tokens × 50 levels × 32 bytes = 1.6MB (20x less memory)
642#[derive(Debug)]
643pub struct OrderBookManager {
644 books: Arc<RwLock<std::collections::HashMap<String, OrderBook>>>, // Token ID -> OrderBook
645 max_depth: usize,
646}
647
648impl OrderBookManager {
649 /// Create a new order book manager
650 /// Starts with an empty collection of books
651 pub fn new(max_depth: usize) -> Self {
652 Self {
653 books: Arc::new(RwLock::new(std::collections::HashMap::new())),
654 max_depth,
655 }
656 }
657
658 /// Get or create an order book for a token
659 /// If we don't have a book for this token yet, create a new empty one
660 pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
661 let mut books = self.books.write().map_err(|_| {
662 PolyfillError::internal_simple("Failed to acquire book lock")
663 })?;
664
665 if let Some(book) = books.get(token_id) {
666 Ok(book.clone()) // Return a copy of the existing book
667 } else {
668 // Create a new book for this token
669 let book = OrderBook::new(token_id.to_string(), self.max_depth);
670 books.insert(token_id.to_string(), book.clone());
671 Ok(book)
672 }
673 }
674
675 /// Update a book with a delta
676 /// This is called when we receive real-time updates from the exchange
677 pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
678 let mut books = self.books.write().map_err(|_| {
679 PolyfillError::internal_simple("Failed to acquire book lock")
680 })?;
681
682 // Find the book for this token (must already exist)
683 let book = books
684 .get_mut(&delta.token_id)
685 .ok_or_else(|| {
686 PolyfillError::market_data(
687 format!("No book found for token: {}", delta.token_id),
688 crate::errors::MarketDataErrorKind::TokenNotFound,
689 )
690 })?;
691
692 // Apply the update to the specific book
693 book.apply_delta(delta)
694 }
695
696 /// Get a book snapshot
697 /// Returns a copy of the current book state that won't change
698 pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
699 let books = self.books.read().map_err(|_| {
700 PolyfillError::internal_simple("Failed to acquire book lock")
701 })?;
702
703 books
704 .get(token_id)
705 .map(|book| book.snapshot()) // Create a snapshot copy
706 .ok_or_else(|| {
707 PolyfillError::market_data(
708 format!("No book found for token: {}", token_id),
709 crate::errors::MarketDataErrorKind::TokenNotFound,
710 )
711 })
712 }
713
714 /// Get all available books
715 /// Returns snapshots of every book we're currently tracking
716 pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
717 let books = self.books.read().map_err(|_| {
718 PolyfillError::internal_simple("Failed to acquire book lock")
719 })?;
720
721 Ok(books.values().map(|book| book.snapshot()).collect())
722 }
723
724 /// Remove stale books
725 /// Cleans up books that haven't been updated recently (probably disconnected)
726 /// This prevents memory leaks from accumulating dead books
727 pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
728 let mut books = self.books.write().map_err(|_| {
729 PolyfillError::internal_simple("Failed to acquire book lock")
730 })?;
731
732 let initial_count = books.len();
733 books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books
734 let removed = initial_count - books.len();
735
736 if removed > 0 {
737 debug!("Removed {} stale order books", removed);
738 }
739
740 Ok(removed)
741 }
742}
743
744/// Order book analytics and statistics
745/// Provides a summary view of the book's health and characteristics
746#[derive(Debug, Clone)]
747pub struct BookAnalytics {
748 pub token_id: String,
749 pub timestamp: chrono::DateTime<Utc>,
750 pub bid_count: usize, // How many different bid price levels
751 pub ask_count: usize, // How many different ask price levels
752 pub total_bid_size: Decimal, // Total size of all bids combined
753 pub total_ask_size: Decimal, // Total size of all asks combined
754 pub spread: Option<Decimal>, // Current spread (ask - bid)
755 pub spread_pct: Option<Decimal>, // Spread as percentage
756 pub mid_price: Option<Decimal>, // Current mid price
757 pub volatility: Option<Decimal>, // Price volatility (if calculated)
758}
759
760impl OrderBook {
761 /// Calculate analytics for this book
762 /// Gives you a quick health check of the market
763 pub fn analytics(&self) -> BookAnalytics {
764 let bid_count = self.bids.len();
765 let ask_count = self.asks.len();
766 // Sum up all bid/ask sizes, converting from fixed-point back to Decimal
767 let total_bid_size_units: i64 = self.bids.values().sum();
768 let total_ask_size_units: i64 = self.asks.values().sum();
769 let total_bid_size = qty_to_decimal(total_bid_size_units);
770 let total_ask_size = qty_to_decimal(total_ask_size_units);
771
772 BookAnalytics {
773 token_id: self.token_id.clone(),
774 timestamp: self.timestamp,
775 bid_count,
776 ask_count,
777 total_bid_size,
778 total_ask_size,
779 spread: self.spread(),
780 spread_pct: self.spread_pct(),
781 mid_price: self.mid_price(),
782 volatility: self.calculate_volatility(),
783 }
784 }
785
786 /// Calculate price volatility (simplified)
787 /// This is a placeholder - real volatility needs historical price data
788 fn calculate_volatility(&self) -> Option<Decimal> {
789 // This is a simplified volatility calculation
790 // In a real implementation, you'd want to track price history over time
791 // and calculate standard deviation of price changes
792 None
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use rust_decimal_macros::dec;
800 use std::str::FromStr;
801 use std::time::Duration; // Convenient macro for creating Decimal literals
802
803 #[test]
804 fn test_order_book_creation() {
805 // Test that we can create a new empty order book
806 let book = OrderBook::new("test_token".to_string(), 10);
807 assert_eq!(book.token_id, "test_token");
808 assert_eq!(book.bids.len(), 0); // Should start empty
809 assert_eq!(book.asks.len(), 0); // Should start empty
810 }
811
812 #[test]
813 fn test_apply_delta() {
814 // Test that we can apply order book updates
815 let mut book = OrderBook::new("test_token".to_string(), 10);
816
817 // Create a buy order at $0.50 for 100 tokens
818 let delta = OrderDelta {
819 token_id: "test_token".to_string(),
820 timestamp: Utc::now(),
821 side: Side::BUY,
822 price: dec!(0.5),
823 size: dec!(100),
824 sequence: 1,
825 };
826
827 book.apply_delta(delta).unwrap();
828 assert_eq!(book.sequence, 1); // Sequence should update
829 assert_eq!(book.best_bid().unwrap().price, dec!(0.5)); // Should be our bid
830 assert_eq!(book.best_bid().unwrap().size, dec!(100)); // Should be our size
831 }
832
833 #[test]
834 fn test_spread_calculation() {
835 // Test that we can calculate the spread between bid and ask
836 let mut book = OrderBook::new("test_token".to_string(), 10);
837
838 // Add a bid at $0.50
839 book.apply_delta(OrderDelta {
840 token_id: "test_token".to_string(),
841 timestamp: Utc::now(),
842 side: Side::BUY,
843 price: dec!(0.5),
844 size: dec!(100),
845 sequence: 1,
846 }).unwrap();
847
848 // Add an ask at $0.52
849 book.apply_delta(OrderDelta {
850 token_id: "test_token".to_string(),
851 timestamp: Utc::now(),
852 side: Side::SELL,
853 price: dec!(0.52),
854 size: dec!(100),
855 sequence: 2,
856 }).unwrap();
857
858 let spread = book.spread().unwrap();
859 assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02
860 }
861
862 #[test]
863 fn test_market_impact() {
864 // Test market impact calculation for a large order
865 let mut book = OrderBook::new("test_token".to_string(), 10);
866
867 // Add multiple ask levels (people selling at different prices)
868 // $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens
869 for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
870 book.apply_delta(OrderDelta {
871 token_id: "test_token".to_string(),
872 timestamp: Utc::now(),
873 side: Side::SELL,
874 price: *price,
875 size: dec!(100),
876 sequence: i as u64 + 1,
877 }).unwrap();
878 }
879
880 // Try to buy 150 tokens (will need to hit multiple price levels)
881 let impact = book.calculate_market_impact(Side::BUY, dec!(150)).unwrap();
882 assert!(impact.average_price > dec!(0.50)); // Should be worse than best price
883 assert!(impact.average_price < dec!(0.51)); // But not as bad as second level
884 }
885
886 #[test]
887 fn test_apply_bid_delta_legacy() {
888 let mut book = OrderBook::new("test_token".to_string(), 10);
889
890 // Test adding a bid
891 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
892
893 let best_bid = book.best_bid();
894 assert!(best_bid.is_some());
895 let bid = best_bid.unwrap();
896 assert_eq!(bid.price, Decimal::from_str("0.75").unwrap());
897 assert_eq!(bid.size, Decimal::from_str("100.0").unwrap());
898
899 // Test updating the bid
900 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("150.0").unwrap());
901 let updated_bid = book.best_bid().unwrap();
902 assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap());
903
904 // Test removing the bid
905 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO);
906 assert!(book.best_bid().is_none());
907 }
908
909 #[test]
910 fn test_apply_ask_delta_legacy() {
911 let mut book = OrderBook::new("test_token".to_string(), 10);
912
913 // Test adding an ask
914 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("50.0").unwrap());
915
916 let best_ask = book.best_ask();
917 assert!(best_ask.is_some());
918 let ask = best_ask.unwrap();
919 assert_eq!(ask.price, Decimal::from_str("0.76").unwrap());
920 assert_eq!(ask.size, Decimal::from_str("50.0").unwrap());
921
922 // Test updating the ask
923 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("75.0").unwrap());
924 let updated_ask = book.best_ask().unwrap();
925 assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap());
926
927 // Test removing the ask
928 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO);
929 assert!(book.best_ask().is_none());
930 }
931
932 #[test]
933 fn test_liquidity_analysis() {
934 let mut book = OrderBook::new("test_token".to_string(), 10);
935
936 // Build order book using legacy methods
937 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
938 book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap());
939 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
940 book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("120.0").unwrap());
941
942 // Test liquidity at specific price - when buying, we look at ask liquidity
943 let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY);
944 assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap());
945
946 // Test liquidity at specific price - when selling, we look at bid liquidity
947 let sell_liquidity = book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
948 assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap());
949
950 // Test liquidity in range - when buying, we look at ask liquidity in range
951 let buy_range_liquidity = book.liquidity_in_range(
952 Decimal::from_str("0.74").unwrap(),
953 Decimal::from_str("0.77").unwrap(),
954 Side::BUY
955 );
956 // Should include ask liquidity: 80 (0.76 ask) + 120 (0.77 ask) = 200
957 assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap());
958
959 // Test liquidity in range - when selling, we look at bid liquidity in range
960 let sell_range_liquidity = book.liquidity_in_range(
961 Decimal::from_str("0.74").unwrap(),
962 Decimal::from_str("0.77").unwrap(),
963 Side::SELL
964 );
965 // Should include bid liquidity: 50 (0.74 bid) + 100 (0.75 bid) = 150
966 assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap());
967 }
968
969 #[test]
970 fn test_book_validation() {
971 let mut book = OrderBook::new("test_token".to_string(), 10);
972
973 // Empty book should be valid
974 assert!(book.is_valid());
975
976 // Add normal levels
977 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
978 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
979 assert!(book.is_valid());
980
981 // Create crossed book (invalid) - bid higher than ask
982 book.apply_bid_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("50.0").unwrap());
983 assert!(!book.is_valid());
984 }
985
986 #[test]
987 fn test_book_staleness() {
988 let mut book = OrderBook::new("test_token".to_string(), 10);
989
990 // Fresh book should not be stale
991 assert!(!book.is_stale(Duration::from_secs(60))); // 60 second threshold
992
993 // Add some data
994 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
995 assert!(!book.is_stale(Duration::from_secs(60)));
996
997 // Note: We can't easily test actual staleness without manipulating time,
998 // but we can test the method exists and works with fresh data
999 }
1000
1001 #[test]
1002 fn test_depth_management() {
1003 let mut book = OrderBook::new("test_token".to_string(), 3); // Only 3 levels
1004
1005 // Add multiple levels
1006 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
1007 book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap());
1008 book.apply_bid_delta(Decimal::from_str("0.73").unwrap(), Decimal::from_str("20.0").unwrap());
1009
1010 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
1011 book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("40.0").unwrap());
1012 book.apply_ask_delta(Decimal::from_str("0.78").unwrap(), Decimal::from_str("30.0").unwrap());
1013
1014 // Should have levels on each side
1015 let bids = book.bids(Some(3));
1016 let asks = book.asks(Some(3));
1017
1018 assert!(bids.len() <= 3);
1019 assert!(asks.len() <= 3);
1020
1021 // Best levels should be there
1022 assert_eq!(book.best_bid().unwrap().price, Decimal::from_str("0.75").unwrap());
1023 assert_eq!(book.best_ask().unwrap().price, Decimal::from_str("0.76").unwrap());
1024 }
1025
1026 #[test]
1027 fn test_fast_operations() {
1028 let mut book = OrderBook::new("test_token".to_string(), 10);
1029
1030 // Test using legacy methods which call fast operations internally
1031 book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
1032 book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
1033
1034 let best_bid_fast = book.best_bid_fast();
1035 let best_ask_fast = book.best_ask_fast();
1036
1037 assert!(best_bid_fast.is_some());
1038 assert!(best_ask_fast.is_some());
1039
1040 // Test fast spread and mid price
1041 let spread_fast = book.spread_fast();
1042 let mid_fast = book.mid_price_fast();
1043
1044 assert!(spread_fast.is_some()); // Should have a spread
1045 assert!(mid_fast.is_some()); // Should have a mid price
1046 }
1047}