feat(book): apply websocket book updates

This commit is contained in:
floor-licker
2026-01-30 18:17:45 -05:00
parent dea50163fe
commit 9594058fd5
2 changed files with 106 additions and 0 deletions
+69
View File
@@ -396,6 +396,75 @@ impl OrderBook {
Ok(()) Ok(())
} }
/// Apply a WebSocket `book` update for this token.
///
/// The official Polymarket CLOB WebSocket `book` event contains batches of
/// price levels for both sides. Unlike `apply_delta_fast`, this method can
/// apply many levels that share the same message timestamp.
///
/// Notes:
/// - This performs upserts (update/insert/remove) for the provided levels.
/// - It does **not** infer removals for levels omitted from the message.
/// - Insertions of *new* price levels may allocate (BTreeMap node growth).
pub fn apply_book_update(&mut self, update: &BookUpdate) -> Result<()> {
if update.asset_id != self.token_id {
return Err(PolyfillError::validation("Token ID mismatch"));
}
// Use the exchange-provided timestamp as our monotonic sequence marker.
// This is less strict than the REST/legacy delta sequence but works for
// ignoring obviously stale book snapshots.
if update.timestamp <= self.sequence {
return Ok(());
}
self.sequence = update.timestamp;
self.timestamp = chrono::DateTime::<Utc>::from_timestamp(update.timestamp as i64, 0)
.unwrap_or_else(Utc::now);
// Apply bids (BUY) and asks (SELL) as level upserts.
for level in &update.bids {
let price_ticks = decimal_to_price(level.price)
.map_err(|_| PolyfillError::validation("Invalid price"))?;
let size_units = decimal_to_qty(level.size)
.map_err(|_| PolyfillError::validation("Invalid size"))?;
if let Some(tick_size_ticks) = self.tick_size_ticks {
if tick_size_ticks > 0 && !price_ticks.is_multiple_of(tick_size_ticks) {
return Err(PolyfillError::validation("Price not aligned to tick size"));
}
}
if size_units == 0 {
self.bids.remove(&price_ticks);
} else {
self.bids.insert(price_ticks, size_units);
}
}
for level in &update.asks {
let price_ticks = decimal_to_price(level.price)
.map_err(|_| PolyfillError::validation("Invalid price"))?;
let size_units = decimal_to_qty(level.size)
.map_err(|_| PolyfillError::validation("Invalid size"))?;
if let Some(tick_size_ticks) = self.tick_size_ticks {
if tick_size_ticks > 0 && !price_ticks.is_multiple_of(tick_size_ticks) {
return Err(PolyfillError::validation("Price not aligned to tick size"));
}
}
if size_units == 0 {
self.asks.remove(&price_ticks);
} else {
self.asks.insert(price_ticks, size_units);
}
}
self.trim_depth();
Ok(())
}
/// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION /// Apply a bid-side delta (someone wants to buy) - LEGACY VERSION
/// If size is 0, it means "remove this price level entirely" /// If size is 0, it means "remove this price level entirely"
/// Otherwise, set the total size at this price level /// Otherwise, set the total size at this price level
+37
View File
@@ -2,9 +2,11 @@ use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell; use std::cell::Cell;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::str::FromStr;
use chrono::Utc; use chrono::Utc;
use polyfill_rs::{OrderBookImpl, Side}; use polyfill_rs::{OrderBookImpl, Side};
use rust_decimal::Decimal;
thread_local! { thread_local! {
static ALLOCATIONS: Cell<usize> = const { Cell::new(0) }; static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
@@ -127,3 +129,38 @@ fn no_alloc_apply_delta_fast_existing_level_update() {
.unwrap(); .unwrap();
guard.assert_no_allocations(); guard.assert_no_allocations();
} }
#[test]
fn no_alloc_apply_book_update_existing_levels() {
let asset_id = "test_asset_id";
let token_hash = token_id_hash(asset_id);
let mut book = OrderBookImpl::new(asset_id.to_string(), 100);
// Allocate during setup: create initial price levels.
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::SELL, 7600, 1_000_000, 2))
.unwrap();
let update = polyfill_rs::types::BookUpdate {
asset_id: asset_id.to_string(),
market: "0xabc".to_string(),
timestamp: 10,
bids: vec![polyfill_rs::types::OrderSummary {
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("200.0").unwrap(),
}],
asks: vec![polyfill_rs::types::OrderSummary {
price: Decimal::from_str("0.76").unwrap(),
size: Decimal::from_str("50.0").unwrap(),
}],
hash: None,
};
// Warm up TLS access before measuring (defensive).
let _ = allocation_count();
let guard = NoAllocGuard::new();
book.apply_book_update(&update).unwrap();
guard.assert_no_allocations();
}