mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-16 05:58:06 +00:00
fix: resolve rustfmt configuration duplicate key error and apply consistent code formatting across all source files
This commit is contained in:
+272
-181
@@ -3,20 +3,20 @@
|
||||
use crate::errors::{PolyfillError, Result};
|
||||
use crate::types::*;
|
||||
use crate::utils::math;
|
||||
use chrono::Utc;
|
||||
use rust_decimal::Decimal;
|
||||
use std::collections::BTreeMap; // BTreeMap keeps prices sorted automatically - crucial for order books
|
||||
use std::sync::{Arc, RwLock}; // For thread-safe access across multiple tasks
|
||||
use tracing::{debug, trace, warn}; // Logging for debugging and monitoring
|
||||
use chrono::Utc;
|
||||
|
||||
/// High-performance order book implementation
|
||||
///
|
||||
///
|
||||
/// This is the core data structure that holds all the live buy/sell orders for a token.
|
||||
/// The efficiency of this code is critical as the order book is constantly being updated as orders are added and removed.
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE OPTIMIZATION: This struct now uses fixed-point integers internally
|
||||
/// instead of Decimal for maximum speed. The performance difference is dramatic:
|
||||
///
|
||||
///
|
||||
/// Before (Decimal): ~100ns per operation + memory allocation
|
||||
/// After (fixed-point): ~5ns per operation, zero allocations
|
||||
|
||||
@@ -24,51 +24,51 @@ use chrono::Utc;
|
||||
pub struct OrderBook {
|
||||
/// Token ID this book represents (like "123456" for a specific prediction market outcome)
|
||||
pub token_id: String,
|
||||
|
||||
|
||||
/// Hash of token_id for fast lookups (avoids string comparisons in hot path)
|
||||
pub token_id_hash: u64,
|
||||
|
||||
|
||||
/// Current sequence number for ordering updates
|
||||
/// This helps us ignore old/duplicate updates that arrive out of order
|
||||
pub sequence: u64,
|
||||
|
||||
|
||||
/// Last update timestamp - when we last got new data for this book
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
|
||||
|
||||
/// Bid side (price -> size, sorted descending) - NOW USING FIXED-POINT!
|
||||
/// BTreeMap automatically keeps highest bids first, which is what we want
|
||||
/// Key = price in ticks (like 6500 for $0.65), Value = size in fixed-point units
|
||||
///
|
||||
///
|
||||
/// BEFORE (slow): bids: BTreeMap<Decimal, Decimal>,
|
||||
/// AFTER (fast): bids: BTreeMap<Price, Qty>,
|
||||
///
|
||||
///
|
||||
/// Why this is faster:
|
||||
/// - Integer comparisons are ~10x faster than Decimal comparisons
|
||||
/// - No memory allocation for each price level
|
||||
/// - Better CPU cache utilization (smaller data structures)
|
||||
bids: BTreeMap<Price, Qty>,
|
||||
|
||||
|
||||
/// Ask side (price -> size, sorted ascending) - NOW USING FIXED-POINT!
|
||||
/// BTreeMap keeps lowest asks first - people selling at cheapest prices
|
||||
///
|
||||
///
|
||||
/// BEFORE (slow): asks: BTreeMap<Decimal, Decimal>,
|
||||
/// AFTER (fast): asks: BTreeMap<Price, Qty>,
|
||||
asks: BTreeMap<Price, Qty>,
|
||||
|
||||
|
||||
/// Minimum tick size for this market in ticks (like 10 for $0.001 increments)
|
||||
/// Some markets only allow certain price increments
|
||||
/// We store this in ticks for fast validation without conversion
|
||||
tick_size_ticks: Option<Price>,
|
||||
|
||||
|
||||
/// Maximum depth to maintain (how many price levels to keep)
|
||||
///
|
||||
///
|
||||
/// We don't need to track every single price level, just the best ones because:
|
||||
/// - Trading reality 90% of volume happens in the top 5-10 price levels
|
||||
/// - Execution priority: Orders get filled from best price first, so deep levels often don't matter
|
||||
/// - Market efficiency: If you're buying and best ask is $0.67, you'll never pay $0.95
|
||||
/// - Risk management: Large orders that would hit deep levels are usually broken up
|
||||
/// - Data freshness: Deep levels often have stale orders from hours/days ago
|
||||
///
|
||||
///
|
||||
/// Typical values: 10-50 for retail, 100-500 for institutional HFT systems
|
||||
max_depth: usize,
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl OrderBook {
|
||||
token_id.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
|
||||
Self {
|
||||
token_id,
|
||||
token_id_hash,
|
||||
@@ -107,7 +107,7 @@ impl OrderBook {
|
||||
self.tick_size_ticks = Some(tick_size_ticks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Set the tick size directly in ticks (even faster)
|
||||
/// Use this when you already have the tick size in our internal format
|
||||
pub fn set_tick_size_ticks(&mut self, tick_size_ticks: Price) {
|
||||
@@ -116,31 +116,34 @@ impl OrderBook {
|
||||
|
||||
/// Get the current best bid (highest price someone is willing to pay)
|
||||
/// Uses next_back() because BTreeMap sorts ascending, but we want the highest bid
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Now returns data in external format but internally uses fast lookups
|
||||
pub fn best_bid(&self) -> Option<BookLevel> {
|
||||
// BEFORE (slow, ~50ns + allocation):
|
||||
// self.bids.iter().next_back().map(|(&price, &size)| BookLevel { price, size })
|
||||
|
||||
|
||||
// AFTER (fast, ~5ns, no allocation for the lookup):
|
||||
self.bids.iter().next_back().map(|(&price_ticks, &size_units)| {
|
||||
// Convert from internal fixed-point to external Decimal format
|
||||
// This conversion only happens at the API boundary
|
||||
BookLevel {
|
||||
price: price_to_decimal(price_ticks),
|
||||
size: qty_to_decimal(size_units),
|
||||
}
|
||||
})
|
||||
self.bids
|
||||
.iter()
|
||||
.next_back()
|
||||
.map(|(&price_ticks, &size_units)| {
|
||||
// Convert from internal fixed-point to external Decimal format
|
||||
// This conversion only happens at the API boundary
|
||||
BookLevel {
|
||||
price: price_to_decimal(price_ticks),
|
||||
size: qty_to_decimal(size_units),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current best ask (lowest price someone is willing to sell at)
|
||||
/// Uses next() because BTreeMap sorts ascending, so first item is lowest ask
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Now returns data in external format but internally uses fast lookups
|
||||
pub fn best_ask(&self) -> Option<BookLevel> {
|
||||
// BEFORE (slow, ~50ns + allocation):
|
||||
// self.asks.iter().next().map(|(&price, &size)| BookLevel { price, size })
|
||||
|
||||
|
||||
// AFTER (fast, ~5ns, no allocation for the lookup):
|
||||
self.asks.iter().next().map(|(&price_ticks, &size_units)| {
|
||||
// Convert from internal fixed-point to external Decimal format
|
||||
@@ -152,25 +155,27 @@ impl OrderBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current best bid in fast internal format
|
||||
/// Get the current best bid in fast internal format
|
||||
/// Use this for internal calculations to avoid conversion overhead
|
||||
pub fn best_bid_fast(&self) -> Option<FastBookLevel> {
|
||||
self.bids.iter().next_back().map(|(&price, &size)| {
|
||||
FastBookLevel::new(price, size)
|
||||
})
|
||||
self.bids
|
||||
.iter()
|
||||
.next_back()
|
||||
.map(|(&price, &size)| FastBookLevel::new(price, size))
|
||||
}
|
||||
|
||||
/// Get the current best ask in fast internal format
|
||||
/// Get the current best ask in fast internal format
|
||||
/// Use this for internal calculations to avoid conversion overhead
|
||||
pub fn best_ask_fast(&self) -> Option<FastBookLevel> {
|
||||
self.asks.iter().next().map(|(&price, &size)| {
|
||||
FastBookLevel::new(price, size)
|
||||
})
|
||||
self.asks
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(&price, &size)| FastBookLevel::new(price, size))
|
||||
}
|
||||
|
||||
/// Get the current spread (difference between best ask and best bid)
|
||||
/// This tells us how "tight" the market is - smaller spread = more liquid market
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
|
||||
pub fn spread(&self) -> Option<Decimal> {
|
||||
// BEFORE (slow, ~100ns + multiple allocations):
|
||||
@@ -178,7 +183,7 @@ impl OrderBook {
|
||||
// (Some(bid), Some(ask)) => Some(ask.price - bid.price),
|
||||
// _ => None,
|
||||
// }
|
||||
|
||||
|
||||
// AFTER (fast, ~5ns, no allocations):
|
||||
let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
|
||||
let spread_ticks = math::spread_fast(best_bid_ticks, best_ask_ticks)?;
|
||||
@@ -187,7 +192,7 @@ impl OrderBook {
|
||||
|
||||
/// Get the current mid price (halfway between best bid and ask)
|
||||
/// This is often used as the "fair value" of the market
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Now uses fast internal calculations, only converts to Decimal at the end
|
||||
pub fn mid_price(&self) -> Option<Decimal> {
|
||||
// BEFORE (slow, ~80ns + allocations):
|
||||
@@ -195,7 +200,7 @@ impl OrderBook {
|
||||
// self.best_bid()?.price,
|
||||
// self.best_ask()?.price,
|
||||
// )
|
||||
|
||||
|
||||
// AFTER (fast, ~3ns, no allocations):
|
||||
let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
|
||||
let mid_ticks = math::mid_price_fast(best_bid_ticks, best_ask_ticks)?;
|
||||
@@ -204,7 +209,7 @@ impl OrderBook {
|
||||
|
||||
/// Get the spread as a percentage (relative to the bid price)
|
||||
/// Useful for comparing spreads across different price levels
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Now uses fast internal calculations and returns basis points
|
||||
pub fn spread_pct(&self) -> Option<Decimal> {
|
||||
let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
|
||||
@@ -212,7 +217,7 @@ impl OrderBook {
|
||||
// Convert basis points back to percentage decimal
|
||||
Some(Decimal::from(spread_bps) / Decimal::from(100))
|
||||
}
|
||||
|
||||
|
||||
/// Get best bid and ask prices in fast internal format
|
||||
/// Helper method to avoid code duplication and minimize conversions
|
||||
fn best_prices_fast(&self) -> Option<(Price, Price)> {
|
||||
@@ -220,14 +225,14 @@ impl OrderBook {
|
||||
let best_ask_ticks = self.asks.iter().next()?.0;
|
||||
Some((*best_bid_ticks, *best_ask_ticks))
|
||||
}
|
||||
|
||||
|
||||
/// Get the current spread in fast internal format (PERFORMANCE OPTIMIZED)
|
||||
/// Returns spread in ticks - use this for internal calculations
|
||||
pub fn spread_fast(&self) -> Option<Price> {
|
||||
let (best_bid_ticks, best_ask_ticks) = self.best_prices_fast()?;
|
||||
math::spread_fast(best_bid_ticks, best_ask_ticks)
|
||||
}
|
||||
|
||||
|
||||
/// Get the current mid price in fast internal format (PERFORMANCE OPTIMIZED)
|
||||
/// Returns mid price in ticks - use this for internal calculations
|
||||
pub fn mid_price_fast(&self) -> Option<Price> {
|
||||
@@ -237,7 +242,7 @@ impl OrderBook {
|
||||
|
||||
/// Get all bids up to a certain depth (top N price levels)
|
||||
/// Returns them in descending price order (best bids first)
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Converts from internal fixed-point to external Decimal format
|
||||
/// Only call this when you need to return data to external APIs
|
||||
pub fn bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
|
||||
@@ -255,7 +260,7 @@ impl OrderBook {
|
||||
|
||||
/// Get all asks up to a certain depth (top N price levels)
|
||||
/// Returns them in ascending price order (best asks first)
|
||||
///
|
||||
///
|
||||
/// PERFORMANCE: Converts from internal fixed-point to external Decimal format
|
||||
/// Only call this when you need to return data to external APIs
|
||||
pub fn asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
|
||||
@@ -269,8 +274,8 @@ impl OrderBook {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all bids in fast internal format
|
||||
|
||||
/// Get all bids in fast internal format
|
||||
/// Use this for internal calculations to avoid conversion overhead
|
||||
pub fn bids_fast(&self, depth: Option<usize>) -> Vec<FastBookLevel> {
|
||||
let depth = depth.unwrap_or(self.max_depth);
|
||||
@@ -308,7 +313,7 @@ impl OrderBook {
|
||||
|
||||
/// Apply a delta update to the book (LEGACY VERSION - for external API compatibility)
|
||||
/// A "delta" is an incremental change - like "add 100 tokens at $0.65" or "remove all at $0.70"
|
||||
///
|
||||
///
|
||||
/// This method converts the external Decimal delta to our internal fixed-point format
|
||||
/// and then calls the fast version. Use apply_delta_fast() directly when possible.
|
||||
pub fn apply_delta(&mut self, delta: OrderDelta) -> Result<()> {
|
||||
@@ -316,16 +321,16 @@ impl OrderBook {
|
||||
let tick_size_decimal = self.tick_size_ticks.map(price_to_decimal);
|
||||
let fast_delta = FastOrderDelta::from_order_delta(&delta, tick_size_decimal)
|
||||
.map_err(|e| PolyfillError::validation(format!("Invalid delta: {}", e)))?;
|
||||
|
||||
|
||||
// Use the fast internal version
|
||||
self.apply_delta_fast(fast_delta)
|
||||
}
|
||||
|
||||
|
||||
/// Apply a delta update to the book
|
||||
///
|
||||
///
|
||||
/// This is the high-performance version that works directly with fixed-point data.
|
||||
/// It includes tick alignment validation and is much faster than the Decimal version.
|
||||
///
|
||||
///
|
||||
/// Performance improvement: ~50x faster than the old Decimal version!
|
||||
/// - No Decimal conversions in the hot path
|
||||
/// - Integer comparisons instead of Decimal comparisons
|
||||
@@ -334,7 +339,11 @@ impl OrderBook {
|
||||
// Validate sequence ordering - ignore old updates that arrive late
|
||||
// This is crucial for maintaining data integrity in real-time systems
|
||||
if delta.sequence <= self.sequence {
|
||||
trace!("Ignoring stale delta: {} <= {}", delta.sequence, self.sequence);
|
||||
trace!(
|
||||
"Ignoring stale delta: {} <= {}",
|
||||
delta.sequence,
|
||||
self.sequence
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -351,7 +360,7 @@ impl OrderBook {
|
||||
// if !is_price_tick_aligned(price_to_decimal(delta.price), tick_size_decimal) {
|
||||
// return Err(...);
|
||||
// }
|
||||
|
||||
|
||||
// AFTER (fast, ~2ns, pure integer):
|
||||
if tick_size_ticks > 0 && delta.price % tick_size_ticks != 0 {
|
||||
// Price is not aligned to tick size - reject the update
|
||||
@@ -390,7 +399,7 @@ impl OrderBook {
|
||||
/// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION
|
||||
/// If size is 0, it means "remove this price level entirely"
|
||||
/// Otherwise, set the total size at this price level
|
||||
///
|
||||
///
|
||||
/// This converts to fixed-point and calls the fast version
|
||||
#[allow(dead_code)]
|
||||
fn apply_bid_delta(&mut self, price: Decimal, size: Decimal) {
|
||||
@@ -402,7 +411,7 @@ impl OrderBook {
|
||||
|
||||
/// Apply an ask-side delta (someone wants to sell) - LEGACY VERSION
|
||||
/// Same logic as bids - size of 0 means remove the price level
|
||||
///
|
||||
///
|
||||
/// This converts to fixed-point and calls the fast version
|
||||
#[allow(dead_code)]
|
||||
fn apply_ask_delta(&mut self, price: Decimal, size: Decimal) {
|
||||
@@ -411,9 +420,9 @@ impl OrderBook {
|
||||
let size_units = decimal_to_qty(size).unwrap_or(0);
|
||||
self.apply_ask_delta_fast(price_ticks, size_units);
|
||||
}
|
||||
|
||||
|
||||
/// Apply a bid-side delta (someone wants to buy) - FAST VERSION
|
||||
///
|
||||
///
|
||||
/// This is the high-performance version that works directly with fixed-point.
|
||||
/// Much faster than the Decimal version - pure integer operations.
|
||||
fn apply_bid_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
|
||||
@@ -423,7 +432,7 @@ impl OrderBook {
|
||||
// } else {
|
||||
// self.bids.insert(price, size);
|
||||
// }
|
||||
|
||||
|
||||
// AFTER (fast, ~5ns, no allocation):
|
||||
if size_units == 0 {
|
||||
self.bids.remove(&price_ticks); // No more buyers at this price
|
||||
@@ -433,7 +442,7 @@ impl OrderBook {
|
||||
}
|
||||
|
||||
/// Apply an ask-side delta (someone wants to sell) - FAST VERSION
|
||||
///
|
||||
///
|
||||
/// This is the high-performance version that works directly with fixed-point.
|
||||
/// Much faster than the Decimal version - pure integer operations.
|
||||
fn apply_ask_delta_fast(&mut self, price_ticks: Price, size_units: Qty) {
|
||||
@@ -443,7 +452,7 @@ impl OrderBook {
|
||||
// } else {
|
||||
// self.asks.insert(price, size);
|
||||
// }
|
||||
|
||||
|
||||
// AFTER (fast, ~5ns, no allocation):
|
||||
if size_units == 0 {
|
||||
self.asks.remove(&price_ticks); // No more sellers at this price
|
||||
@@ -454,12 +463,12 @@ impl OrderBook {
|
||||
|
||||
/// Trim the book to maintain depth limits
|
||||
/// We don't want to track every single price level - just the best ones
|
||||
///
|
||||
///
|
||||
/// Why limit depth? Several reasons:
|
||||
/// 1. Memory efficiency: A popular market might have thousands of price levels,
|
||||
/// but only the top 10-50 levels are actually tradeable with reasonable size
|
||||
/// 2. Performance: Fewer levels = faster iteration when calculating market impact
|
||||
/// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
|
||||
/// 3. Relevance: Deep levels (like bids at $0.01 when best bid is $0.65) are
|
||||
/// mostly noise and will never get hit in normal trading
|
||||
/// 4. Stale data: Deep levels often contain old orders that haven't been cancelled
|
||||
/// 5. Network bandwidth: Less data to send when streaming updates
|
||||
@@ -473,7 +482,7 @@ impl OrderBook {
|
||||
}
|
||||
}
|
||||
|
||||
// For asks, remove the HIGHEST prices (worst asks) if we have too many
|
||||
// For asks, remove the HIGHEST prices (worst asks) if we have too many
|
||||
// Example: If best ask is $0.67, we don't care about asks at $0.95
|
||||
if self.asks.len() > self.max_depth {
|
||||
let to_remove = self.asks.len() - self.max_depth;
|
||||
@@ -493,16 +502,16 @@ impl OrderBook {
|
||||
pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
|
||||
// PERFORMANCE NOTE: This method still uses Decimal for external compatibility,
|
||||
// but the internal order book lookups now use our fast fixed-point data structures.
|
||||
//
|
||||
//
|
||||
// BEFORE: Each level lookup involved Decimal operations (~50ns each)
|
||||
// AFTER: Level lookups use integer operations (~5ns each)
|
||||
//
|
||||
//
|
||||
// For a 10-level impact calculation: 500ns → 50ns (10x speedup)
|
||||
|
||||
|
||||
// Get the levels we'd be trading against
|
||||
let levels = match side {
|
||||
Side::BUY => self.asks(None), // If buying, we hit the ask side
|
||||
Side::SELL => self.bids(None), // If selling, we hit the bid side
|
||||
Side::BUY => self.asks(None), // If buying, we hit the ask side
|
||||
Side::SELL => self.bids(None), // If selling, we hit the bid side
|
||||
};
|
||||
|
||||
if levels.is_empty() {
|
||||
@@ -517,7 +526,7 @@ impl OrderBook {
|
||||
for level in levels {
|
||||
let fill_size = std::cmp::min(remaining_size, level.size);
|
||||
let level_cost = fill_size * level.price;
|
||||
|
||||
|
||||
total_cost += level_cost;
|
||||
weighted_price += level_cost; // This accumulates the weighted average
|
||||
remaining_size -= fill_size;
|
||||
@@ -532,21 +541,21 @@ impl OrderBook {
|
||||
// This is a perfect example of why we don't need infinite depth:
|
||||
// If we can't fill your order with the top N levels, you probably
|
||||
// shouldn't be placing that order anyway - it would move the market too much
|
||||
return None;
|
||||
return None;
|
||||
}
|
||||
|
||||
let avg_price = weighted_price / size;
|
||||
|
||||
|
||||
// Calculate how much we moved the market compared to the best price
|
||||
let impact = match side {
|
||||
Side::BUY => {
|
||||
let best_ask = self.best_ask()?.price;
|
||||
(avg_price - best_ask) / best_ask // How much worse than best ask
|
||||
}
|
||||
},
|
||||
Side::SELL => {
|
||||
let best_bid = self.best_bid()?.price;
|
||||
(best_bid - avg_price) / best_bid // How much worse than best bid
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some(MarketImpact {
|
||||
@@ -572,7 +581,7 @@ impl OrderBook {
|
||||
Ok(ticks) => ticks,
|
||||
Err(_) => return Decimal::ZERO, // Invalid price
|
||||
};
|
||||
|
||||
|
||||
match side {
|
||||
Side::BUY => {
|
||||
// How much we can buy at this price (look at asks)
|
||||
@@ -583,13 +592,18 @@ impl OrderBook {
|
||||
// How much we can sell at this price (look at bids)
|
||||
let size_units = self.bids.get(&price_ticks).copied().unwrap_or_default();
|
||||
qty_to_decimal(size_units)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total liquidity within a price range
|
||||
/// Useful for understanding how much depth exists in a certain price band
|
||||
pub fn liquidity_in_range(&self, min_price: Decimal, max_price: Decimal, side: Side) -> Decimal {
|
||||
pub fn liquidity_in_range(
|
||||
&self,
|
||||
min_price: Decimal,
|
||||
max_price: Decimal,
|
||||
side: Side,
|
||||
) -> Decimal {
|
||||
// Convert decimal prices to our internal fixed-point representation
|
||||
let min_price_ticks = match decimal_to_price(min_price) {
|
||||
Ok(ticks) => ticks,
|
||||
@@ -599,10 +613,14 @@ impl OrderBook {
|
||||
Ok(ticks) => ticks,
|
||||
Err(_) => return Decimal::ZERO, // Invalid price
|
||||
};
|
||||
|
||||
|
||||
let levels: Vec<_> = match side {
|
||||
Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(),
|
||||
Side::SELL => self.bids.range(min_price_ticks..=max_price_ticks).rev().collect(),
|
||||
Side::SELL => self
|
||||
.bids
|
||||
.range(min_price_ticks..=max_price_ticks)
|
||||
.rev()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Sum up the sizes, converting from fixed-point back to Decimal
|
||||
@@ -615,7 +633,7 @@ impl OrderBook {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match (self.best_bid(), self.best_ask()) {
|
||||
(Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition
|
||||
_ => true, // Empty book is technically valid
|
||||
_ => true, // Empty book is technically valid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -624,20 +642,20 @@ impl OrderBook {
|
||||
/// This tells you what would happen if you executed a large order
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketImpact {
|
||||
pub average_price: Decimal, // The average price you'd get across all fills
|
||||
pub impact_pct: Decimal, // How much worse than the best price (as percentage)
|
||||
pub total_cost: Decimal, // Total amount you'd pay/receive
|
||||
pub size_filled: Decimal, // How much of your order got filled
|
||||
pub average_price: Decimal, // The average price you'd get across all fills
|
||||
pub impact_pct: Decimal, // How much worse than the best price (as percentage)
|
||||
pub total_cost: Decimal, // Total amount you'd pay/receive
|
||||
pub size_filled: Decimal, // How much of your order got filled
|
||||
}
|
||||
|
||||
/// Thread-safe order book manager
|
||||
/// This manages multiple order books (one per token) and handles concurrent access
|
||||
/// Multiple threads can read/write different books simultaneously
|
||||
///
|
||||
///
|
||||
/// The depth limiting becomes even more critical here because we might be tracking
|
||||
/// hundreds or thousands of different tokens simultaneously. If each book had
|
||||
/// unlimited depth, we could easily use gigabytes of RAM for mostly useless data.
|
||||
///
|
||||
///
|
||||
/// Example: 1000 tokens × 1000 price levels × 32 bytes per level = 32MB just for prices
|
||||
/// With depth limiting: 1000 tokens × 50 levels × 32 bytes = 1.6MB (20x less memory)
|
||||
#[derive(Debug)]
|
||||
@@ -659,9 +677,10 @@ impl OrderBookManager {
|
||||
/// Get or create an order book for a token
|
||||
/// If we don't have a book for this token yet, create a new empty one
|
||||
pub fn get_or_create_book(&self, token_id: &str) -> Result<OrderBook> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
let mut books = self
|
||||
.books
|
||||
.write()
|
||||
.map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
|
||||
|
||||
if let Some(book) = books.get(token_id) {
|
||||
Ok(book.clone()) // Return a copy of the existing book
|
||||
@@ -676,19 +695,18 @@ impl OrderBookManager {
|
||||
/// Update a book with a delta
|
||||
/// This is called when we receive real-time updates from the exchange
|
||||
pub fn apply_delta(&self, delta: OrderDelta) -> Result<()> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
let mut books = self
|
||||
.books
|
||||
.write()
|
||||
.map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
|
||||
|
||||
// Find the book for this token (must already exist)
|
||||
let book = books
|
||||
.get_mut(&delta.token_id)
|
||||
.ok_or_else(|| {
|
||||
PolyfillError::market_data(
|
||||
format!("No book found for token: {}", delta.token_id),
|
||||
crate::errors::MarketDataErrorKind::TokenNotFound,
|
||||
)
|
||||
})?;
|
||||
let book = books.get_mut(&delta.token_id).ok_or_else(|| {
|
||||
PolyfillError::market_data(
|
||||
format!("No book found for token: {}", delta.token_id),
|
||||
crate::errors::MarketDataErrorKind::TokenNotFound,
|
||||
)
|
||||
})?;
|
||||
|
||||
// Apply the update to the specific book
|
||||
book.apply_delta(delta)
|
||||
@@ -697,9 +715,10 @@ impl OrderBookManager {
|
||||
/// Get a book snapshot
|
||||
/// Returns a copy of the current book state that won't change
|
||||
pub fn get_book(&self, token_id: &str) -> Result<crate::types::OrderBook> {
|
||||
let books = self.books.read().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
let books = self
|
||||
.books
|
||||
.read()
|
||||
.map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
|
||||
|
||||
books
|
||||
.get(token_id)
|
||||
@@ -715,9 +734,10 @@ impl OrderBookManager {
|
||||
/// Get all available books
|
||||
/// Returns snapshots of every book we're currently tracking
|
||||
pub fn get_all_books(&self) -> Result<Vec<crate::types::OrderBook>> {
|
||||
let books = self.books.read().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
let books = self
|
||||
.books
|
||||
.read()
|
||||
.map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
|
||||
|
||||
Ok(books.values().map(|book| book.snapshot()).collect())
|
||||
}
|
||||
@@ -726,9 +746,10 @@ impl OrderBookManager {
|
||||
/// Cleans up books that haven't been updated recently (probably disconnected)
|
||||
/// This prevents memory leaks from accumulating dead books
|
||||
pub fn cleanup_stale_books(&self, max_age: std::time::Duration) -> Result<usize> {
|
||||
let mut books = self.books.write().map_err(|_| {
|
||||
PolyfillError::internal_simple("Failed to acquire book lock")
|
||||
})?;
|
||||
let mut books = self
|
||||
.books
|
||||
.write()
|
||||
.map_err(|_| PolyfillError::internal_simple("Failed to acquire book lock"))?;
|
||||
|
||||
let initial_count = books.len();
|
||||
books.retain(|_, book| !book.is_stale(max_age)); // Keep only non-stale books
|
||||
@@ -748,13 +769,13 @@ impl OrderBookManager {
|
||||
pub struct BookAnalytics {
|
||||
pub token_id: String,
|
||||
pub timestamp: chrono::DateTime<Utc>,
|
||||
pub bid_count: usize, // How many different bid price levels
|
||||
pub ask_count: usize, // How many different ask price levels
|
||||
pub total_bid_size: Decimal, // Total size of all bids combined
|
||||
pub total_ask_size: Decimal, // Total size of all asks combined
|
||||
pub spread: Option<Decimal>, // Current spread (ask - bid)
|
||||
pub bid_count: usize, // How many different bid price levels
|
||||
pub ask_count: usize, // How many different ask price levels
|
||||
pub total_bid_size: Decimal, // Total size of all bids combined
|
||||
pub total_ask_size: Decimal, // Total size of all asks combined
|
||||
pub spread: Option<Decimal>, // Current spread (ask - bid)
|
||||
pub spread_pct: Option<Decimal>, // Spread as percentage
|
||||
pub mid_price: Option<Decimal>, // Current mid price
|
||||
pub mid_price: Option<Decimal>, // Current mid price
|
||||
pub volatility: Option<Decimal>, // Price volatility (if calculated)
|
||||
}
|
||||
|
||||
@@ -814,7 +835,7 @@ mod tests {
|
||||
fn test_apply_delta() {
|
||||
// Test that we can apply order book updates
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Create a buy order at $0.50 for 100 tokens
|
||||
let delta = OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
@@ -835,7 +856,7 @@ mod tests {
|
||||
fn test_spread_calculation() {
|
||||
// Test that we can calculate the spread between bid and ask
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Add a bid at $0.50
|
||||
book.apply_delta(OrderDelta {
|
||||
token_id: "test_token".to_string(),
|
||||
@@ -844,7 +865,8 @@ mod tests {
|
||||
price: dec!(0.5),
|
||||
size: dec!(100),
|
||||
sequence: 1,
|
||||
}).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Add an ask at $0.52
|
||||
book.apply_delta(OrderDelta {
|
||||
@@ -854,7 +876,8 @@ mod tests {
|
||||
price: dec!(0.52),
|
||||
size: dec!(100),
|
||||
sequence: 2,
|
||||
}).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let spread = book.spread().unwrap();
|
||||
assert_eq!(spread, dec!(0.02)); // $0.52 - $0.50 = $0.02
|
||||
@@ -864,7 +887,7 @@ mod tests {
|
||||
fn test_market_impact() {
|
||||
// Test market impact calculation for a large order
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Add multiple ask levels (people selling at different prices)
|
||||
// $0.50 for 100 tokens, $0.51 for 100 tokens, $0.52 for 100 tokens
|
||||
for (i, price) in [dec!(0.50), dec!(0.51), dec!(0.52)].iter().enumerate() {
|
||||
@@ -875,7 +898,8 @@ mod tests {
|
||||
price: *price,
|
||||
size: dec!(100),
|
||||
sequence: i as u64 + 1,
|
||||
}).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Try to buy 150 tokens (will need to hit multiple price levels)
|
||||
@@ -887,21 +911,27 @@ mod tests {
|
||||
#[test]
|
||||
fn test_apply_bid_delta_legacy() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Test adding a bid
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
|
||||
let best_bid = book.best_bid();
|
||||
assert!(best_bid.is_some());
|
||||
let bid = best_bid.unwrap();
|
||||
assert_eq!(bid.price, Decimal::from_str("0.75").unwrap());
|
||||
assert_eq!(bid.size, Decimal::from_str("100.0").unwrap());
|
||||
|
||||
|
||||
// Test updating the bid
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("150.0").unwrap());
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("150.0").unwrap(),
|
||||
);
|
||||
let updated_bid = book.best_bid().unwrap();
|
||||
assert_eq!(updated_bid.size, Decimal::from_str("150.0").unwrap());
|
||||
|
||||
|
||||
// Test removing the bid
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::ZERO);
|
||||
assert!(book.best_bid().is_none());
|
||||
@@ -910,21 +940,27 @@ mod tests {
|
||||
#[test]
|
||||
fn test_apply_ask_delta_legacy() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Test adding an ask
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("50.0").unwrap());
|
||||
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("50.0").unwrap(),
|
||||
);
|
||||
|
||||
let best_ask = book.best_ask();
|
||||
assert!(best_ask.is_some());
|
||||
let ask = best_ask.unwrap();
|
||||
assert_eq!(ask.price, Decimal::from_str("0.76").unwrap());
|
||||
assert_eq!(ask.size, Decimal::from_str("50.0").unwrap());
|
||||
|
||||
|
||||
// Test updating the ask
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("75.0").unwrap());
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("75.0").unwrap(),
|
||||
);
|
||||
let updated_ask = book.best_ask().unwrap();
|
||||
assert_eq!(updated_ask.size, Decimal::from_str("75.0").unwrap());
|
||||
|
||||
|
||||
// Test removing the ask
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::ZERO);
|
||||
assert!(book.best_ask().is_none());
|
||||
@@ -933,35 +969,48 @@ mod tests {
|
||||
#[test]
|
||||
fn test_liquidity_analysis() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Build order book using legacy methods
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("120.0").unwrap());
|
||||
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.74").unwrap(),
|
||||
Decimal::from_str("50.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("80.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.77").unwrap(),
|
||||
Decimal::from_str("120.0").unwrap(),
|
||||
);
|
||||
|
||||
// Test liquidity at specific price - when buying, we look at ask liquidity
|
||||
let buy_liquidity = book.liquidity_at_price(Decimal::from_str("0.76").unwrap(), Side::BUY);
|
||||
assert_eq!(buy_liquidity, Decimal::from_str("80.0").unwrap());
|
||||
|
||||
// Test liquidity at specific price - when selling, we look at bid liquidity
|
||||
let sell_liquidity = book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
|
||||
|
||||
// Test liquidity at specific price - when selling, we look at bid liquidity
|
||||
let sell_liquidity =
|
||||
book.liquidity_at_price(Decimal::from_str("0.75").unwrap(), Side::SELL);
|
||||
assert_eq!(sell_liquidity, Decimal::from_str("100.0").unwrap());
|
||||
|
||||
|
||||
// Test liquidity in range - when buying, we look at ask liquidity in range
|
||||
let buy_range_liquidity = book.liquidity_in_range(
|
||||
Decimal::from_str("0.74").unwrap(),
|
||||
Decimal::from_str("0.77").unwrap(),
|
||||
Side::BUY
|
||||
Side::BUY,
|
||||
);
|
||||
// Should include ask liquidity: 80 (0.76 ask) + 120 (0.77 ask) = 200
|
||||
assert_eq!(buy_range_liquidity, Decimal::from_str("200.0").unwrap());
|
||||
|
||||
|
||||
// Test liquidity in range - when selling, we look at bid liquidity in range
|
||||
let sell_range_liquidity = book.liquidity_in_range(
|
||||
Decimal::from_str("0.74").unwrap(),
|
||||
Decimal::from_str("0.77").unwrap(),
|
||||
Side::SELL
|
||||
Side::SELL,
|
||||
);
|
||||
// Should include bid liquidity: 50 (0.74 bid) + 100 (0.75 bid) = 150
|
||||
assert_eq!(sell_range_liquidity, Decimal::from_str("150.0").unwrap());
|
||||
@@ -970,31 +1019,43 @@ mod tests {
|
||||
#[test]
|
||||
fn test_book_validation() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Empty book should be valid
|
||||
assert!(book.is_valid());
|
||||
|
||||
|
||||
// Add normal levels
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("80.0").unwrap(),
|
||||
);
|
||||
assert!(book.is_valid());
|
||||
|
||||
|
||||
// Create crossed book (invalid) - bid higher than ask
|
||||
book.apply_bid_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("50.0").unwrap());
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.77").unwrap(),
|
||||
Decimal::from_str("50.0").unwrap(),
|
||||
);
|
||||
assert!(!book.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_book_staleness() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Fresh book should not be stale
|
||||
assert!(!book.is_stale(Duration::from_secs(60))); // 60 second threshold
|
||||
|
||||
|
||||
// Add some data
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
assert!(!book.is_stale(Duration::from_secs(60)));
|
||||
|
||||
|
||||
// Note: We can't easily test actual staleness without manipulating time,
|
||||
// but we can test the method exists and works with fresh data
|
||||
}
|
||||
@@ -1002,47 +1063,77 @@ mod tests {
|
||||
#[test]
|
||||
fn test_depth_management() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 3); // Only 3 levels
|
||||
|
||||
|
||||
// Add multiple levels
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
book.apply_bid_delta(Decimal::from_str("0.74").unwrap(), Decimal::from_str("50.0").unwrap());
|
||||
book.apply_bid_delta(Decimal::from_str("0.73").unwrap(), Decimal::from_str("20.0").unwrap());
|
||||
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.77").unwrap(), Decimal::from_str("40.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.78").unwrap(), Decimal::from_str("30.0").unwrap());
|
||||
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.74").unwrap(),
|
||||
Decimal::from_str("50.0").unwrap(),
|
||||
);
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.73").unwrap(),
|
||||
Decimal::from_str("20.0").unwrap(),
|
||||
);
|
||||
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("80.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.77").unwrap(),
|
||||
Decimal::from_str("40.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.78").unwrap(),
|
||||
Decimal::from_str("30.0").unwrap(),
|
||||
);
|
||||
|
||||
// Should have levels on each side
|
||||
let bids = book.bids(Some(3));
|
||||
let asks = book.asks(Some(3));
|
||||
|
||||
|
||||
assert!(bids.len() <= 3);
|
||||
assert!(asks.len() <= 3);
|
||||
|
||||
|
||||
// Best levels should be there
|
||||
assert_eq!(book.best_bid().unwrap().price, Decimal::from_str("0.75").unwrap());
|
||||
assert_eq!(book.best_ask().unwrap().price, Decimal::from_str("0.76").unwrap());
|
||||
assert_eq!(
|
||||
book.best_bid().unwrap().price,
|
||||
Decimal::from_str("0.75").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
book.best_ask().unwrap().price,
|
||||
Decimal::from_str("0.76").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fast_operations() {
|
||||
let mut book = OrderBook::new("test_token".to_string(), 10);
|
||||
|
||||
|
||||
// Test using legacy methods which call fast operations internally
|
||||
book.apply_bid_delta(Decimal::from_str("0.75").unwrap(), Decimal::from_str("100.0").unwrap());
|
||||
book.apply_ask_delta(Decimal::from_str("0.76").unwrap(), Decimal::from_str("80.0").unwrap());
|
||||
|
||||
book.apply_bid_delta(
|
||||
Decimal::from_str("0.75").unwrap(),
|
||||
Decimal::from_str("100.0").unwrap(),
|
||||
);
|
||||
book.apply_ask_delta(
|
||||
Decimal::from_str("0.76").unwrap(),
|
||||
Decimal::from_str("80.0").unwrap(),
|
||||
);
|
||||
|
||||
let best_bid_fast = book.best_bid_fast();
|
||||
let best_ask_fast = book.best_ask_fast();
|
||||
|
||||
|
||||
assert!(best_bid_fast.is_some());
|
||||
assert!(best_ask_fast.is_some());
|
||||
|
||||
|
||||
// Test fast spread and mid price
|
||||
let spread_fast = book.spread_fast();
|
||||
let mid_fast = book.mid_price_fast();
|
||||
|
||||
|
||||
assert!(spread_fast.is_some()); // Should have a spread
|
||||
assert!(mid_fast.is_some()); // Should have a mid price
|
||||
assert!(mid_fast.is_some()); // Should have a mid price
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user