mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-26 10:58:07 +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
|
/// 2. Use a different trading strategy
|
||||||
/// 3. Accept that there's not enough liquidity right now
|
/// 3. Accept that there's not enough liquidity right now
|
||||||
pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
|
pub fn calculate_market_impact(&self, side: Side, size: Decimal) -> Option<MarketImpact> {
|
||||||
// PERFORMANCE NOTE: This method still uses Decimal for external compatibility,
|
let size_units = decimal_to_qty(size).ok()?;
|
||||||
// but the internal order book lookups now use our fast fixed-point data structures.
|
let (filled_units, total_notional, best_price_ticks) = match side {
|
||||||
//
|
Side::BUY => fill_market_impact(self.asks.iter(), size_units)?,
|
||||||
// BEFORE: Each level lookup involved Decimal operations (~50ns each)
|
Side::SELL => fill_market_impact(self.bids.iter().rev(), size_units)?,
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if levels.is_empty() {
|
let total_cost = Decimal::from_i128_with_scale(total_notional, 8);
|
||||||
return None; // No liquidity available
|
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 mut remaining_size = size;
|
let impact = if side == Side::BUY {
|
||||||
let mut total_cost = Decimal::ZERO;
|
(avg_price - best_price) / best_price
|
||||||
let mut weighted_price = Decimal::ZERO;
|
} else {
|
||||||
|
(best_price - avg_price) / best_price
|
||||||
// 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
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(MarketImpact {
|
Some(MarketImpact {
|
||||||
average_price: avg_price,
|
average_price: avg_price,
|
||||||
impact_pct: impact,
|
impact_pct: impact,
|
||||||
total_cost,
|
total_cost,
|
||||||
size_filled: size,
|
size_filled: filled_size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,28 +749,69 @@ impl OrderBook {
|
|||||||
Err(_) => return Decimal::ZERO, // Invalid price
|
Err(_) => return Decimal::ZERO, // Invalid price
|
||||||
};
|
};
|
||||||
|
|
||||||
let levels: Vec<_> = match side {
|
let total_size_units: Qty = match side {
|
||||||
Side::BUY => self.asks.range(min_price_ticks..=max_price_ticks).collect(),
|
Side::BUY => self
|
||||||
|
.asks
|
||||||
|
.range(min_price_ticks..=max_price_ticks)
|
||||||
|
.map(|(_, level)| level.qty)
|
||||||
|
.sum(),
|
||||||
Side::SELL => self
|
Side::SELL => self
|
||||||
.bids
|
.bids
|
||||||
.range(min_price_ticks..=max_price_ticks)
|
.range(min_price_ticks..=max_price_ticks)
|
||||||
.rev()
|
.map(|(_, level)| level.qty)
|
||||||
.collect(),
|
.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)
|
qty_to_decimal(total_size_units)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that prices are properly ordered
|
/// Validate that prices are properly ordered
|
||||||
/// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
|
/// A healthy book should have best bid < best ask (otherwise there's an arbitrage opportunity)
|
||||||
pub fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool {
|
||||||
match (self.best_bid(), self.best_ask()) {
|
self.best_prices_fast()
|
||||||
(Some(bid), Some(ask)) => bid.price < ask.price, // Normal market condition
|
.map(|(best_bid_ticks, best_ask_ticks)| best_bid_ticks < best_ask_ticks)
|
||||||
_ => true, // Empty book is technically valid
|
.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
|
/// Market impact calculation result
|
||||||
|
|||||||
@@ -112,6 +112,47 @@ fn no_alloc_mid_and_spread_fast() {
|
|||||||
guard.assert_no_allocations();
|
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]
|
#[test]
|
||||||
fn no_alloc_apply_delta_fast_existing_level_update() {
|
fn no_alloc_apply_delta_fast_existing_level_update() {
|
||||||
let token_id = "test_token";
|
let token_id = "test_token";
|
||||||
|
|||||||
Reference in New Issue
Block a user