mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-25 10:28:07 +00:00
Optimize WS book decimal parsing
This commit is contained in:
+176
-11
@@ -9,10 +9,21 @@
|
|||||||
|
|
||||||
use crate::book::OrderBookManager;
|
use crate::book::OrderBookManager;
|
||||||
use crate::errors::{PolyfillError, Result};
|
use crate::errors::{PolyfillError, Result};
|
||||||
use crate::types::{decimal_to_price, decimal_to_qty, Side};
|
use crate::types::{Price, Qty, Side, MAX_QTY};
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use simd_json::prelude::*;
|
use simd_json::prelude::*;
|
||||||
use std::str::FromStr;
|
|
||||||
|
const WS_DECIMAL_SCALE_DIGITS: usize = 4;
|
||||||
|
const WS_SCALE_FACTOR: u64 = 10_000;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ParseScaledError {
|
||||||
|
Empty,
|
||||||
|
InvalidChar,
|
||||||
|
MultipleDots,
|
||||||
|
TooManyDecimals,
|
||||||
|
Overflow,
|
||||||
|
ZeroPrice,
|
||||||
|
}
|
||||||
|
|
||||||
/// Summary of what happened while processing a WS payload.
|
/// Summary of what happened while processing a WS payload.
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -157,6 +168,85 @@ fn parse_u64<'tape, 'input>(value: simd_json::tape::Value<'tape, 'input>) -> Opt
|
|||||||
.or_else(|| value.into_string().and_then(|s| s.parse::<u64>().ok()))
|
.or_else(|| value.into_string().and_then(|s| s.parse::<u64>().ok()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_price_ticks_ascii(s: &str) -> std::result::Result<Price, ParseScaledError> {
|
||||||
|
let ticks = parse_scaled_ascii_u64(s, Price::MAX as u64)?;
|
||||||
|
if ticks == 0 {
|
||||||
|
return Err(ParseScaledError::ZeroPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ticks as Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_qty_units_ascii(s: &str) -> std::result::Result<Qty, ParseScaledError> {
|
||||||
|
let units = parse_scaled_ascii_u64(s, MAX_QTY as u64)?;
|
||||||
|
Ok(units as Qty)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_scaled_ascii_u64(s: &str, max: u64) -> std::result::Result<u64, ParseScaledError> {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(ParseScaledError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut int = 0u64;
|
||||||
|
let mut frac = 0u64;
|
||||||
|
let mut frac_digits = 0usize;
|
||||||
|
let mut seen_dot = false;
|
||||||
|
let mut seen_digit = false;
|
||||||
|
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => {
|
||||||
|
seen_digit = true;
|
||||||
|
let d = u64::from(b - b'0');
|
||||||
|
if seen_dot {
|
||||||
|
if frac_digits < WS_DECIMAL_SCALE_DIGITS {
|
||||||
|
frac = frac
|
||||||
|
.checked_mul(10)
|
||||||
|
.and_then(|x| x.checked_add(d))
|
||||||
|
.ok_or(ParseScaledError::Overflow)?;
|
||||||
|
frac_digits += 1;
|
||||||
|
} else if d != 0 {
|
||||||
|
return Err(ParseScaledError::TooManyDecimals);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int = int
|
||||||
|
.checked_mul(10)
|
||||||
|
.and_then(|x| x.checked_add(d))
|
||||||
|
.ok_or(ParseScaledError::Overflow)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
b'.' if !seen_dot => {
|
||||||
|
seen_dot = true;
|
||||||
|
},
|
||||||
|
b'.' => return Err(ParseScaledError::MultipleDots),
|
||||||
|
_ => return Err(ParseScaledError::InvalidChar),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seen_digit {
|
||||||
|
return Err(ParseScaledError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
while frac_digits < WS_DECIMAL_SCALE_DIGITS {
|
||||||
|
frac = frac.checked_mul(10).ok_or(ParseScaledError::Overflow)?;
|
||||||
|
frac_digits += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaled = int
|
||||||
|
.checked_mul(WS_SCALE_FACTOR)
|
||||||
|
.and_then(|x| x.checked_add(frac))
|
||||||
|
.ok_or(ParseScaledError::Overflow)?;
|
||||||
|
|
||||||
|
if scaled > max {
|
||||||
|
return Err(ParseScaledError::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(scaled)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_levels<'tape, 'input>(
|
fn apply_levels<'tape, 'input>(
|
||||||
book: &mut crate::book::OrderBook,
|
book: &mut crate::book::OrderBook,
|
||||||
side: Side,
|
side: Side,
|
||||||
@@ -177,15 +267,10 @@ fn apply_levels<'tape, 'input>(
|
|||||||
.and_then(|v| v.into_string())
|
.and_then(|v| v.into_string())
|
||||||
.ok_or_else(|| PolyfillError::parse("Missing size", None))?;
|
.ok_or_else(|| PolyfillError::parse("Missing size", None))?;
|
||||||
|
|
||||||
let price_decimal =
|
let price_ticks = parse_price_ticks_ascii(price_str)
|
||||||
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"))?;
|
.map_err(|_| PolyfillError::validation("Invalid price"))?;
|
||||||
let size_units =
|
let size_units = parse_qty_units_ascii(size_str)
|
||||||
decimal_to_qty(size_decimal).map_err(|_| PolyfillError::validation("Invalid size"))?;
|
.map_err(|_| PolyfillError::validation("Invalid size"))?;
|
||||||
|
|
||||||
book.apply_ws_book_level_fast(side, price_ticks, size_units)?;
|
book.apply_ws_book_level_fast(side, price_ticks, size_units)?;
|
||||||
applied += 1;
|
applied += 1;
|
||||||
@@ -193,3 +278,83 @@ fn apply_levels<'tape, 'input>(
|
|||||||
|
|
||||||
Ok(applied)
|
Ok(applied)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{parse_price_ticks_ascii, parse_qty_units_ascii, ParseScaledError};
|
||||||
|
use crate::{OrderBookManager, WsBookUpdateProcessor};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_fixed_scale_prices_without_decimal() {
|
||||||
|
assert_eq!(parse_price_ticks_ascii("0.7134"), Ok(7134));
|
||||||
|
assert_eq!(parse_price_ticks_ascii("12.5000"), Ok(125_000));
|
||||||
|
assert_eq!(parse_price_ticks_ascii("12.5"), Ok(125_000));
|
||||||
|
assert_eq!(parse_price_ticks_ascii("1"), Ok(10_000));
|
||||||
|
assert_eq!(parse_price_ticks_ascii(".5"), Ok(5_000));
|
||||||
|
assert_eq!(parse_price_ticks_ascii("1.230000"), Ok(12_300));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_fixed_scale_sizes_without_decimal() {
|
||||||
|
assert_eq!(parse_qty_units_ascii("0"), Ok(0));
|
||||||
|
assert_eq!(parse_qty_units_ascii("100.0000"), Ok(1_000_000));
|
||||||
|
assert_eq!(parse_qty_units_ascii("12.5000"), Ok(125_000));
|
||||||
|
assert_eq!(parse_qty_units_ascii("12.5"), Ok(125_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_scaled_ascii_values() {
|
||||||
|
assert_eq!(parse_price_ticks_ascii(""), Err(ParseScaledError::Empty));
|
||||||
|
assert_eq!(parse_price_ticks_ascii("."), Err(ParseScaledError::Empty));
|
||||||
|
assert_eq!(
|
||||||
|
parse_price_ticks_ascii("0"),
|
||||||
|
Err(ParseScaledError::ZeroPrice)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_price_ticks_ascii("0.0000"),
|
||||||
|
Err(ParseScaledError::ZeroPrice)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_price_ticks_ascii("0.00005"),
|
||||||
|
Err(ParseScaledError::TooManyDecimals)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_price_ticks_ascii("1.23456"),
|
||||||
|
Err(ParseScaledError::TooManyDecimals)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_price_ticks_ascii("1.2.3"),
|
||||||
|
Err(ParseScaledError::MultipleDots)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_qty_units_ascii("-1.0"),
|
||||||
|
Err(ParseScaledError::InvalidChar)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_qty_units_ascii("1e3"),
|
||||||
|
Err(ParseScaledError::InvalidChar)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_qty_units_ascii("922337203685478.0"),
|
||||||
|
Err(ParseScaledError::Overflow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ws_book_processor_rejects_fractional_values_that_would_round() {
|
||||||
|
let asset_id = "test_asset_id";
|
||||||
|
let manager = OrderBookManager::new(10);
|
||||||
|
manager.get_or_create_book(asset_id).unwrap();
|
||||||
|
|
||||||
|
let mut processor = WsBookUpdateProcessor::new(1024);
|
||||||
|
let mut msg = format!(
|
||||||
|
"{{\"event_type\":\"book\",\"asset_id\":\"{asset_id}\",\"market\":\"0xabc\",\"timestamp\":10,\"bids\":[{{\"price\":\"0.75005\",\"size\":\"200.0\"}}],\"asks\":[]}}"
|
||||||
|
)
|
||||||
|
.into_bytes();
|
||||||
|
|
||||||
|
let err = processor
|
||||||
|
.process_bytes(msg.as_mut_slice(), &manager)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("Invalid price"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user