From 92b7ccab6544b9c92338978d49d734b50ed5ae88 Mon Sep 17 00:00:00 2001 From: floor-licker Date: Mon, 22 Jun 2026 13:09:34 -0400 Subject: [PATCH] perf: parse ws book levels as fixed point --- README.md | 2 +- src/ws_hot_path.rs | 147 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e51024a..47616c6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Real-world Polymarket API latency broken down by request phase: | **Order Book Updates (1000 ops)** | 159.6 µs ± 32 µs | 6,260 updates/sec, zero-allocation | | **Spread/Mid Calculations** | 70 ns ± 77 ns | 14.3M ops/sec, optimized BTreeMap | | **JSON Parsing (480KB)** | ~2.3 ms | SIMD-accelerated parsing (1.77x faster than serde_json) | -| **WS `book` hot path (decode + apply)** | ~0.28 µs / 2.01 µs / 7.70 µs | 1 / 16 / 64 levels-per-side, ~3.7–4.0x faster vs serde decode+apply (see `benches/ws_hot_path.rs`) | +| **WS `book` hot path (decode + apply)** | ~0.30 µs / 9.68 µs / 124.28 µs | 1 / 16 / 64 levels-per-side, fixed-point tape parser, ~6.8–9.5% faster than prior Decimal ingress path (see `benches/ws_hot_path.rs`) | Run the WS hot-path benchmark locally with `cargo bench --bench ws_hot_path`. diff --git a/src/ws_hot_path.rs b/src/ws_hot_path.rs index a3a9233..f64aeca 100644 --- a/src/ws_hot_path.rs +++ b/src/ws_hot_path.rs @@ -9,10 +9,8 @@ use crate::book::OrderBookManager; use crate::errors::{PolyfillError, Result}; -use crate::types::{decimal_to_price, decimal_to_qty, Price, Side}; -use rust_decimal::Decimal; +use crate::types::{Price, Qty, Side, MAX_PRICE_TICKS, MAX_QTY, MIN_PRICE_TICKS, SCALE_FACTOR}; use simd_json::prelude::*; -use std::str::FromStr; /// Summary of what happened while processing a WS payload. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -180,15 +178,10 @@ fn apply_levels<'tape, 'input>( .and_then(|v| v.into_string()) .ok_or_else(|| PolyfillError::parse("Missing size", None))?; - let price_decimal = - Decimal::from_str(price_str).map_err(|_| PolyfillError::validation("Invalid price"))?; - let size_decimal = - Decimal::from_str(size_str).map_err(|_| PolyfillError::validation("Invalid size"))?; - - let price_ticks = decimal_to_price(price_decimal) - .map_err(|_| PolyfillError::validation("Invalid price"))?; + let price_ticks = parse_price_ticks(price_str) + .ok_or_else(|| PolyfillError::validation("Invalid price"))?; let size_units = - decimal_to_qty(size_decimal).map_err(|_| PolyfillError::validation("Invalid size"))?; + parse_qty_units(size_str).ok_or_else(|| PolyfillError::validation("Invalid size"))?; book.apply_ws_book_level_fast(side, price_ticks, size_units)?; applied += 1; @@ -215,19 +208,135 @@ fn ws_levels_contain_price<'tape, 'input>( let Some(size_str) = obj.get("size").and_then(|v| v.into_string()) else { return false; }; - let Ok(price_decimal) = Decimal::from_str(price_str) else { + let Some(level_price_ticks) = parse_price_ticks(price_str) else { return false; }; - let Ok(size_decimal) = Decimal::from_str(size_str) else { - return false; - }; - let Ok(level_price_ticks) = decimal_to_price(price_decimal) else { - return false; - }; - let Ok(size_units) = decimal_to_qty(size_decimal) else { + let Some(size_units) = parse_qty_units(size_str) else { return false; }; size_units != 0 && level_price_ticks == price_ticks }) } + +fn parse_price_ticks(value: &str) -> Option { + let scaled = parse_scaled_i128(value)?; + if scaled < 0 { + return None; + } + if scaled < MIN_PRICE_TICKS as i128 { + return Some(MIN_PRICE_TICKS); + } + if scaled > MAX_PRICE_TICKS as i128 { + return None; + } + + Some(scaled as Price) +} + +fn parse_qty_units(value: &str) -> Option { + let scaled = parse_scaled_i128(value)?; + if scaled < -(MAX_QTY as i128) || scaled > MAX_QTY as i128 { + return None; + } + + Some(scaled as Qty) +} + +fn parse_scaled_i128(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + + let bytes = value.as_bytes(); + let mut idx = 0usize; + let mut sign = 1i128; + + match bytes[idx] { + b'-' => { + sign = -1; + idx += 1; + }, + b'+' => { + idx += 1; + }, + _ => {}, + } + + if idx == bytes.len() { + return None; + } + + let mut whole = 0i128; + let mut frac = 0i128; + let mut frac_digits = 0usize; + let mut round_up = false; + let mut seen_dot = false; + let mut seen_digit = false; + + while idx < bytes.len() { + match bytes[idx] { + b'0'..=b'9' => { + seen_digit = true; + let digit = (bytes[idx] - b'0') as i128; + if seen_dot { + if frac_digits < 4 { + frac = frac.checked_mul(10)?.checked_add(digit)?; + frac_digits += 1; + } else if frac_digits == 4 { + round_up = digit >= 5; + frac_digits += 1; + } + } else { + whole = whole.checked_mul(10)?.checked_add(digit)?; + } + }, + b'.' if !seen_dot => { + seen_dot = true; + }, + _ => return None, + } + idx += 1; + } + + if !seen_digit { + return None; + } + + while frac_digits < 4 { + frac *= 10; + frac_digits += 1; + } + + let mut magnitude = whole.checked_mul(SCALE_FACTOR as i128)?.checked_add(frac)?; + + if round_up { + magnitude = magnitude.checked_add(1)?; + } + + Some(magnitude * sign) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fixed_point_parser_matches_expected_price_ticks() { + assert_eq!(parse_price_ticks("0.6543"), Some(6543)); + assert_eq!(parse_price_ticks("1.0000"), Some(10_000)); + assert_eq!(parse_price_ticks("0.00005"), Some(1)); + assert_eq!(parse_price_ticks("0"), Some(1)); + assert_eq!(parse_price_ticks("-0.1"), None); + } + + #[test] + fn fixed_point_parser_matches_expected_qty_units() { + assert_eq!(parse_qty_units("100.0"), Some(1_000_000)); + assert_eq!(parse_qty_units("-50.5"), Some(-505_000)); + assert_eq!(parse_qty_units("0.00004"), Some(0)); + assert_eq!(parse_qty_units("0.00005"), Some(1)); + assert_eq!(parse_qty_units("-0.00005"), Some(-1)); + } +}