mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-25 10:28:07 +00:00
feat(book): apply websocket book updates
This commit is contained in:
+69
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user