mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
perf: avoid allocations in book analysis APIs
This commit is contained in:
+63
-64
@@ -669,69 +669,27 @@ impl OrderBook {
|
||||
/// 2. Use a different trading strategy
|
||||
/// 3. Accept that there's not enough liquidity right now
|
||||
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
|
||||
let size_units = decimal_to_qty(size).ok()?;
|
||||
let (filled_units, total_notional, best_price_ticks) = match side {
|
||||
Side::BUY => fill_market_impact(self.asks.iter(), size_units)?,
|
||||
Side::SELL => fill_market_impact(self.bids.iter().rev(), size_units)?,
|
||||
};
|
||||
|
||||
if levels.is_empty() {
|
||||
return None; // No liquidity available
|
||||
}
|
||||
|
||||
let mut remaining_size = size;
|
||||
let mut total_cost = Decimal::ZERO;
|
||||
let mut weighted_price = Decimal::ZERO;
|
||||
|
||||
// Walk through each price level, filling as much as we can
|
||||
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;
|
||||
|
||||
if remaining_size.is_zero() {
|
||||
break; // We've filled our entire order
|
||||
}
|
||||
}
|
||||
|
||||
if remaining_size > Decimal::ZERO {
|
||||
// Not enough liquidity to fill the whole order
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
let total_cost = Decimal::from_i128_with_scale(total_notional, 8);
|
||||
let filled_size = qty_to_decimal(filled_units);
|
||||
let avg_price = total_cost / filled_size;
|
||||
let best_price = price_to_decimal(best_price_ticks);
|
||||
let impact = if side == Side::BUY {
|
||||
(avg_price - best_price) / best_price
|
||||
} else {
|
||||
(best_price - avg_price) / best_price
|
||||
};
|
||||
|
||||
Some(MarketImpact {
|
||||
average_price: avg_price,
|
||||
impact_pct: impact,
|
||||
total_cost,
|
||||
size_filled: size,
|
||||
size_filled: filled_size,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -791,28 +749,69 @@ impl OrderBook {
|
||||
Err(_) => return Decimal::ZERO, // Invalid price
|
||||
};
|
||||
|
||||
let levels: Vec<_> = match side {
|
||||
Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(),
|
||||
let total_size_units: Qty = match side {
|
||||
Side::BUY => self
|
||||
.asks
|
||||
.range(min_price_ticks..=max_price_ticks)
|
||||
.map(|(_, level)| level.qty)
|
||||
.sum(),
|
||||
Side::SELL => self
|
||||
.bids
|
||||
.range(min_price_ticks..=max_price_ticks)
|
||||
.rev()
|
||||
.collect(),
|
||||
.map(|(_, level)| level.qty)
|
||||
.sum(),
|
||||
};
|
||||
|
||||
// Sum up the sizes, converting from fixed-point back to Decimal
|
||||
let total_size_units: i64 = levels.into_iter().map(|(_, level)| level.qty).sum();
|
||||
qty_to_decimal(total_size_units)
|
||||
}
|
||||
|
||||
/// Validate that prices are properly ordered
|
||||
/// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
|
||||
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
|
||||
self.best_prices_fast()
|
||||
.map(|(best_bid_ticks, best_ask_ticks)| best_bid_ticks < best_ask_ticks)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_market_impact<'a>(
|
||||
levels: impl Iterator<Item = (&'a Price, &'a StoredLevel)>,
|
||||
size_units: Qty,
|
||||
) -> Option<(Qty, i128, Price)> {
|
||||
if size_units <= 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut remaining_units = size_units;
|
||||
let mut filled_units = 0;
|
||||
let mut total_notional = 0i128;
|
||||
let mut best_price_ticks = None;
|
||||
|
||||
for (&price_ticks, level) in levels {
|
||||
if best_price_ticks.is_none() {
|
||||
best_price_ticks = Some(price_ticks);
|
||||
}
|
||||
|
||||
if level.qty <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fill_units = remaining_units.min(level.qty);
|
||||
let fill_notional = (price_ticks as i128).checked_mul(fill_units as i128)?;
|
||||
total_notional = total_notional.checked_add(fill_notional)?;
|
||||
filled_units += fill_units;
|
||||
remaining_units -= fill_units;
|
||||
|
||||
if remaining_units == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if remaining_units > 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((filled_units, total_notional, best_price_ticks?))
|
||||
}
|
||||
|
||||
/// Market impact calculation result
|
||||
|
||||
@@ -112,6 +112,47 @@ fn no_alloc_mid_and_spread_fast() {
|
||||
guard.assert_no_allocations();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_alloc_book_analysis_fast_paths() {
|
||||
let token_id = "test_token";
|
||||
let token_hash = token_id_hash(token_id);
|
||||
let mut book = OrderBookImpl::new(token_id.to_string(), 100);
|
||||
|
||||
book.apply_delta_fast(mk_delta(token_hash, Side::BUY, 7500, 1_000_000, 1))
|
||||
.unwrap();
|
||||
book.apply_delta_fast(mk_delta(token_hash, Side::BUY, 7400, 500_000, 2))
|
||||
.unwrap();
|
||||
book.apply_delta_fast(mk_delta(token_hash, Side::SELL, 7600, 800_000, 3))
|
||||
.unwrap();
|
||||
book.apply_delta_fast(mk_delta(token_hash, Side::SELL, 7700, 1_200_000, 4))
|
||||
.unwrap();
|
||||
|
||||
let impact_size = Decimal::from_str("150.0").unwrap();
|
||||
let min_price = Decimal::from_str("0.74").unwrap();
|
||||
let max_price = Decimal::from_str("0.77").unwrap();
|
||||
let min_average_price = Decimal::from_str("0.76").unwrap();
|
||||
let expected_buy_liquidity = Decimal::from_str("200.0").unwrap();
|
||||
let expected_sell_liquidity = Decimal::from_str("150.0").unwrap();
|
||||
|
||||
let _ = allocation_count();
|
||||
|
||||
let guard = NoAllocGuard::new();
|
||||
let impact = book
|
||||
.calculate_market_impact(Side::BUY, impact_size)
|
||||
.unwrap();
|
||||
assert!(impact.average_price > min_average_price);
|
||||
assert_eq!(
|
||||
book.liquidity_in_range(min_price, max_price, Side::BUY),
|
||||
expected_buy_liquidity
|
||||
);
|
||||
assert_eq!(
|
||||
book.liquidity_in_range(min_price, max_price, Side::SELL),
|
||||
expected_sell_liquidity
|
||||
);
|
||||
assert!(book.is_valid());
|
||||
guard.assert_no_allocations();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_alloc_apply_delta_fast_existing_level_update() {
|
||||
let token_id = "test_token";
|
||||
|
||||
Reference in New Issue
Block a user