From 21d718948b93c931250a39b9d679c40d56270a3a Mon Sep 17 00:00:00 2001 From: kunsonxs Date: Tue, 29 Apr 2025 02:31:40 +0800 Subject: [PATCH 1/2] feat: crypto-bigint replace Price & Quantity --- Cargo.lock | 51 ++++++++++++++++++++++++++++++-- apex-core/Cargo.toml | 2 ++ apex-core/benches/common.rs | 18 +++-------- apex-core/src/engine/engine.rs | 13 ++++---- apex-core/src/engine/types.rs | 36 +++++++++++++--------- apex-core/tests/common.rs | 8 ++--- apex-core/tests/limit_orders.rs | 10 +++---- apex-core/tests/market_orders.rs | 5 ++-- apex-core/tests/modify.rs | 12 +++++--- 9 files changed, 104 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0dd713e..21d13b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,11 @@ dependencies = [ "criterion", "crossbeam", "crossbeam-skiplist", + "crypto-bigint", "flurry", "gnuplot", "mimalloc", + "num-bigint", "rand", ] @@ -274,6 +276,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96272c2ff28b807e09250b180ad1fb7889a3258f7455759b5c3c58b719467130" +dependencies = [ + "num-traits", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "either" version = "1.15.0" @@ -452,6 +465,25 @@ dependencies = [ "libmimalloc-sys", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -574,7 +606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.3", ] [[package]] @@ -584,7 +616,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -744,6 +785,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.100" diff --git a/apex-core/Cargo.toml b/apex-core/Cargo.toml index a061444..eeac308 100644 --- a/apex-core/Cargo.toml +++ b/apex-core/Cargo.toml @@ -12,6 +12,8 @@ mimalloc = { version = "0.1.46" } crossbeam = "0.8" crossbeam-skiplist = "0.1.3" flurry = "0.5.2" +num-bigint = "0.4.6" +crypto-bigint = { version = "0.6.1", features = [] } [dev-dependencies] gnuplot = "0.0.46" diff --git a/apex-core/benches/common.rs b/apex-core/benches/common.rs index 29651a4..c96b39a 100644 --- a/apex-core/benches/common.rs +++ b/apex-core/benches/common.rs @@ -1,31 +1,21 @@ use apex_core::prelude::*; -use crossbeam::epoch; /// Quickly generate a simple limit order for testing -pub fn make_limit_order(id: u64, side: Side, price: Price, qty: Quantity, ts: u64) -> Order { +pub fn make_limit_order(id: u64, side: Side, price: u64, qty: u64, ts: u64) -> Order { let mut value = Order::default(); value.id = id; value.user_id = 1; value.side = side; - value.price = price; - *value.quantity.get_mut() = qty; + value.price = Price::from(price); + *value.quantity.get_mut() = Quantity::from(qty); value.created_at = ts; value.updated_at = ts; value } /// Quickly generate a market order for testing -pub fn make_market_order(id: u64, side: Side, qty: Quantity, ts: u64) -> Order { +pub fn make_market_order(id: u64, side: Side, qty: u64, ts: u64) -> Order { let mut value = make_limit_order(id, side, 0, qty, ts); value.order_type = OrderType::Market; value } - -/// Get the current state of a side of the book -pub fn get_book_state(book: &dyn OrderBookWalker, side: Side) -> Vec<(OrderID, Quantity)> { - let guard = &epoch::pin(); - book.get_book(side) - .iter(guard) - .map(|entry| (entry.value().id, entry.value().quantity())) - .collect() -} diff --git a/apex-core/src/engine/engine.rs b/apex-core/src/engine/engine.rs index c7c81ad..312f191 100644 --- a/apex-core/src/engine/engine.rs +++ b/apex-core/src/engine/engine.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crypto_bigint::Zero; use std::sync::Arc; use std::time::Instant; @@ -72,10 +73,10 @@ impl DefaultMatchingEngine { return WalkingResult::next(); } - remaining_qty = remaining_qty.saturating_sub(maker.quantity()); + remaining_qty = remaining_qty.saturating_sub(&maker.quantity()); order_id_list.push(maker.id); - if remaining_qty == 0 { + if remaining_qty.is_zero().into() { WalkingResult::exit() } else { WalkingResult::next() @@ -85,7 +86,7 @@ impl DefaultMatchingEngine { self.order_book .walking_book_maker(Side::Sell, slippage_price, &mut walking); - if remaining_qty == 0 { + if remaining_qty.is_zero().into() { return Some(order_id_list); } @@ -117,7 +118,7 @@ impl DefaultMatchingEngine { let mut process = |maker: &Order| { let removed = DefaultMatchingEngine::process_order_pair(taker, maker, &mut updated, &mut matched); - WalkingResult::new(removed, taker.quantity() == 0) + WalkingResult::new(removed, taker.quantity().is_zero().into()) }; self.order_book .walking_by_order_id_list(order_id_list_opt.unwrap().as_slice(), &mut process); @@ -158,7 +159,7 @@ impl DefaultMatchingEngine { } let removed = DefaultMatchingEngine::process_order_pair(taker, maker, &mut updated, &mut matched); - WalkingResult::new(removed, taker.quantity() == 0) + WalkingResult::new(removed, taker.quantity().is_zero().into()) }; self.order_book .walking_book_maker(opposite_side, slippage_price, &mut process); @@ -193,7 +194,7 @@ impl DefaultMatchingEngine { } let removed = DefaultMatchingEngine::process_order_pair(taker, maker, &mut updated, &mut matched); - WalkingResult::new(removed, taker.quantity() == 0) + WalkingResult::new(removed, taker.quantity().is_zero().into()) }; self.order_book .walking_book_maker(opposite_side, Some(taker.price), &mut process); diff --git a/apex-core/src/engine/types.rs b/apex-core/src/engine/types.rs index 0bdf878..c4e1e4a 100644 --- a/apex-core/src/engine/types.rs +++ b/apex-core/src/engine/types.rs @@ -1,5 +1,7 @@ +use crypto_bigint::{Limb, NonZero, Reciprocal, U256, U512, Zero}; use mimalloc::MiMalloc; use std::cell::UnsafeCell; +use std::ops::Mul; use std::sync::atomic::{AtomicU8, Ordering}; /// Global allocator @@ -12,11 +14,11 @@ pub type OrderID = u64; /// Price is the type used for prices in the order. /// This is a 64-bit unsigned integer. -pub type Price = u64; +pub type Price = U256; /// Quantity is the type used for quantities in the order. /// This is a 64-bit unsigned integer. -pub type Quantity = u64; +pub type Quantity = U256; /// Priority is that the order book uses to determine the order priority. pub type Priority = u64; @@ -166,7 +168,10 @@ pub struct SlippageTolerance(pub u32); /// Maximum slippage tolerance allowed. /// This is set to 50% (5000 bps). -pub const MAX_SLIPPAGE_TOLERANCE: SlippageTolerance = SlippageTolerance(5000); +pub const MAX_ALLOWED_SLIPPAGE_TOLERANCE: SlippageTolerance = SlippageTolerance(5000); + +/// a constant used for calculating slippage tolerance. +const RECIPROCAL_10000: Reciprocal = Reciprocal::new(NonZero::::new_unwrap(Limb(10_000u64))); /// BookKey is a composite key for identifying an order's position in the book. /// It combines the order's price, priority (timestamp-based), and side (Buy/Sell). @@ -308,10 +313,10 @@ impl Default for Order { match_strategy: MatchStrategy::default(), liquidity_directive: LiquidityDirective::default(), time_in_force: TimeInForce::default(), - price: 0, + price: U256::ZERO, slippage_tolerance: None, - quantity: UnsafeCell::new(0), - filled_quantity: UnsafeCell::new(0), + quantity: UnsafeCell::new(U256::ZERO), + filled_quantity: UnsafeCell::new(U256::ZERO), cancel_reason: UnsafeCell::new(None), reject_reason: UnsafeCell::new(None), created_at: 0, @@ -510,12 +515,15 @@ impl Order { if self.slippage_tolerance.is_none() { return None; } + let slippage = self.slippage_tolerance.unwrap(); - let slippage_bps = slippage.0 as f64 / 10000.0; - let slippage_difference = (self.price as f64 * slippage_bps) as u64; + let mut factor = U512::from(slippage.0); + factor = factor.mul(price); + let (quotient, _) = factor.div_rem_limb_with_reciprocal(&RECIPROCAL_10000); + let (lo, _) = quotient.split(); let bound_price = match self.side { - Side::Buy => price + slippage_difference, - Side::Sell => price - slippage_difference, + Side::Buy => price + lo, + Side::Sell => price - lo, }; Some(bound_price) } @@ -565,7 +573,7 @@ impl Order { } // 4. SlippageTolerance could be None or a valid value if let Some(slippage) = self.slippage_tolerance { - if slippage.0 > MAX_SLIPPAGE_TOLERANCE.0 { + if slippage.0 > MAX_ALLOWED_SLIPPAGE_TOLERANCE.0 { return Err(OrderValidationError::SlippageExceedsMaximum); } } @@ -594,19 +602,19 @@ impl Trade { let mut maker_quantity = maker.quantity(); let mut taker_quantity = taker.quantity(); let traded_quantity = taker_quantity.min(maker_quantity); - if traded_quantity == 0 { + if traded_quantity.is_zero().into() { return None; } maker_quantity = maker.quantity_fill(traded_quantity); taker_quantity = taker.quantity_fill(traded_quantity); - let maker_status = if maker_quantity == 0 { + let maker_status = if maker_quantity.is_zero().into() { OrderStatus::Filled } else { OrderStatus::PartiallyFilled }; - let taker_status = if taker_quantity == 0 { + let taker_status = if taker_quantity.is_zero().into() { OrderStatus::Filled } else { OrderStatus::PartiallyFilled diff --git a/apex-core/tests/common.rs b/apex-core/tests/common.rs index f39cf12..2b46477 100644 --- a/apex-core/tests/common.rs +++ b/apex-core/tests/common.rs @@ -4,20 +4,20 @@ use crossbeam::epoch::default_collector; use crossbeam_skiplist::SkipList; /// Quickly generate a simple limit order for testing -pub fn make_limit_order(id: u64, side: Side, price: Price, qty: Quantity, ts: u64) -> Order { +pub fn make_limit_order(id: u64, side: Side, price: u64, qty: u64, ts: u64) -> Order { let mut value = Order::default(); value.id = id; value.user_id = 1; value.side = side; - value.price = price; - *value.quantity.get_mut() = qty; + value.price = Price::from(price); + *value.quantity.get_mut() = Quantity::from(qty); value.created_at = ts; value.updated_at = ts; value } /// Quickly generate a market order for testing -pub fn make_market_order(id: u64, side: Side, qty: Quantity, ts: u64) -> Order { +pub fn make_market_order(id: u64, side: Side, qty: u64, ts: u64) -> Order { let mut value = make_limit_order(id, side, 0, qty, ts); value.order_type = OrderType::Market; value diff --git a/apex-core/tests/limit_orders.rs b/apex-core/tests/limit_orders.rs index 775be2f..2ea6b8f 100644 --- a/apex-core/tests/limit_orders.rs +++ b/apex-core/tests/limit_orders.rs @@ -112,7 +112,7 @@ fn test_limit_order_multiple_partial_fills() { ); assert_eq!( remaining_sell[0], - (2, 2), + (2, Quantity::from(2u32)), "Sell2 should have 2 remaining units" ); } @@ -179,8 +179,8 @@ fn test_limit_order_partial_and_full_match() { .collect(); assert_eq!(remaining.len(), 2); - assert_eq!(remaining[0], (101, 4)); - assert_eq!(remaining[1], (102, 10)); + assert_eq!(remaining[0], (101, Quantity::from(4u32))); + assert_eq!(remaining[1], (102, Quantity::from(10u32))); } #[test] @@ -216,6 +216,6 @@ fn test_limit_order_iter_continues_after_remove() { .collect(); assert_eq!(remaining.len(), 2); - assert_eq!(remaining[0], (102, 5)); - assert_eq!(remaining[1], (103, 10)); + assert_eq!(remaining[0], (102, Quantity::from(5u32))); + assert_eq!(remaining[1], (103, Quantity::from(10u32))); } diff --git a/apex-core/tests/market_orders.rs b/apex-core/tests/market_orders.rs index 592af74..4a7f245 100644 --- a/apex-core/tests/market_orders.rs +++ b/apex-core/tests/market_orders.rs @@ -134,7 +134,7 @@ fn test_market_order_slippage_exceeded_cancel() { // Create a market buy order with tight slippage let mut buy = make_market_order(3, Side::Buy, 10, 1002); // Wants 10 units - buy.slippage_tolerance = Some(SlippageTolerance(10)); // 0.10% slippage allowed + buy.slippage_tolerance = Some(SlippageTolerance(100)); // 1.00% slippage allowed engine.create_order(&mut buy).unwrap(); engine.match_orders(); @@ -152,7 +152,8 @@ fn test_market_order_slippage_exceeded_cancel() { "Remaining sell order should be sell2 (id=2)" ); assert_eq!( - remaining_sell[0].1, 10, + remaining_sell[0].1, + Quantity::from(10u32), "Sell2 should have full quantity left" ); } diff --git a/apex-core/tests/modify.rs b/apex-core/tests/modify.rs index 8f85f72..41331c4 100644 --- a/apex-core/tests/modify.rs +++ b/apex-core/tests/modify.rs @@ -38,7 +38,9 @@ fn test_update_active_order_price() { let mut buy = make_limit_order(1, Side::Buy, 100, 10, 1000); engine.create_order(&mut buy).unwrap(); - engine.update_order(buy.id, 105, 1001).unwrap(); + engine + .update_order(buy.id, Price::from(105u64), 1001) + .unwrap(); let guard = &epoch::pin(); let buy_book = book.get_book(Side::Buy); @@ -58,7 +60,9 @@ fn test_update_order_priority_after_price_change() { engine.create_order(&mut buy1).unwrap(); engine.create_order(&mut buy2).unwrap(); - engine.update_order(buy1.id, 101, 1002).unwrap(); + engine + .update_order(buy1.id, Price::from(101u64), 1002) + .unwrap(); let state = get_book_state(book.as_ref(), Side::Buy); assert_eq!( @@ -74,7 +78,7 @@ fn test_update_nonexistent_order_should_fail() { let book = Arc::new(DefaultOrderBook::new(id, syncer)); let engine = DefaultMatchingEngine::new(book); - let result = engine.update_order(999, 105, 1001); + let result = engine.update_order(999, Price::from(105u64), 1001); assert!(result.is_err(), "Updating nonexistent order should fail"); } @@ -91,7 +95,7 @@ fn test_update_filled_order_should_fail() { engine.create_order(&mut buy).unwrap(); engine.match_orders(); - let result = engine.update_order(sell.id, 95, 1002); + let result = engine.update_order(sell.id, Price::from(95u64), 1002); assert!(result.is_err(), "Updating filled order should fail"); } From 50774383cbe5c45c48b40fc32f8bd676706126dc Mon Sep 17 00:00:00 2001 From: kunsonxs Date: Tue, 29 Apr 2025 02:36:26 +0800 Subject: [PATCH 2/2] chore: crypto-bigint replace Price & Quantity comments --- apex-core/src/engine/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apex-core/src/engine/types.rs b/apex-core/src/engine/types.rs index c4e1e4a..57d0ed3 100644 --- a/apex-core/src/engine/types.rs +++ b/apex-core/src/engine/types.rs @@ -13,11 +13,11 @@ static GLOBAL: MiMalloc = MiMalloc; pub type OrderID = u64; /// Price is the type used for prices in the order. -/// This is a 64-bit unsigned integer. +/// This is a 256-bit unsigned integer. pub type Price = U256; /// Quantity is the type used for quantities in the order. -/// This is a 64-bit unsigned integer. +/// This is a 256-bit unsigned integer. pub type Quantity = U256; /// Priority is that the order book uses to determine the order priority.