#[cfg(test)]
mod test_order_modifications {
use crate::{OrderBook, OrderBookError};
use pricelevel::{Id, OrderType, OrderUpdate, Price, Quantity, Side, TimeInForce};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_update_price_same_value() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let result = book.add_limit_order(id, price, quantity, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let update = OrderUpdate::UpdatePrice {
order_id: id,
new_price: Price::new(price),
};
let result = book.update_order(update);
assert!(result.is_err());
match result {
Err(OrderBookError::InvalidOperation { message }) => {
assert!(message.contains("Cannot update price to the same value"));
}
_ => panic!("Expected InvalidOperation error"),
}
}
#[test]
fn test_update_price_and_quantity() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let result = book.add_limit_order(id, price, quantity, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let new_price = 1100;
let new_quantity = 15;
let update = OrderUpdate::UpdatePriceAndQuantity {
order_id: id,
new_price: Price::new(new_price),
new_quantity: Quantity::new(new_quantity),
};
let result = book.update_order(update);
assert!(result.is_ok());
let updated_order = book.get_order(id);
assert!(updated_order.is_some());
let updated_order = updated_order.unwrap();
assert_eq!(updated_order.price().as_u128(), new_price);
assert_eq!(
updated_order.visible_quantity(),
Quantity::new(new_quantity)
);
}
#[test]
fn test_cancel_nonexistent_order() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let result = book.cancel_order(id);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_update_order_cancel() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let result = book.add_limit_order(id, price, quantity, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let update = OrderUpdate::Cancel { order_id: id };
let result = book.update_order(update);
assert!(result.is_ok());
let order = book.get_order(id);
assert!(order.is_none());
}
#[test]
fn test_update_order_replace() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let result = book.add_limit_order(id, price, quantity, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let new_price = 1100;
let new_quantity = 15;
let update = OrderUpdate::Replace {
order_id: id,
price: Price::new(new_price),
quantity: Quantity::new(new_quantity),
side: Side::Buy,
};
let result = book.update_order(update);
assert!(result.is_ok());
let replaced_order = book.get_order(id);
assert!(replaced_order.is_some());
let replaced_order = replaced_order.unwrap();
assert_eq!(replaced_order.price().as_u128(), new_price);
assert_eq!(
replaced_order.visible_quantity(),
Quantity::new(new_quantity)
);
}
#[test]
fn test_replace_with_different_side() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let result = book.add_limit_order(id, price, quantity, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let update = OrderUpdate::Replace {
order_id: id,
price: Price::new(1100),
quantity: Quantity::new(15),
side: Side::Sell, };
let result = book.update_order(update);
assert!(result.is_ok());
let replaced_order = book.get_order(id);
assert!(replaced_order.is_some());
assert_eq!(replaced_order.unwrap().side(), Side::Sell);
}
#[test]
fn test_iceberg_order_update_quantity() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let visible = 10;
let hidden = 90;
let side = Side::Buy;
let result =
book.add_iceberg_order(id, price, visible, hidden, side, TimeInForce::Gtc, None);
assert!(result.is_ok());
let new_quantity = 15;
let update = OrderUpdate::UpdateQuantity {
order_id: id,
new_quantity: Quantity::new(new_quantity),
};
let result = book.update_order(update);
assert!(result.is_ok());
let updated_order = book.get_order(id);
assert!(updated_order.is_some());
let updated_order = updated_order.unwrap();
assert_eq!(
updated_order.visible_quantity(),
Quantity::new(new_quantity)
);
match &*updated_order {
OrderType::IcebergOrder {
hidden_quantity, ..
} => {
assert_eq!(hidden_quantity.as_u64(), hidden);
}
_ => panic!("Expected IcebergOrder"),
}
}
}
#[cfg(test)]
mod test_modifications_remaining {
use crate::OrderBook;
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, PegReferenceType, Price, Quantity, Side, TimeInForce,
TimestampMs,
};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_update_price_error_cases() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let update = OrderUpdate::UpdatePrice {
order_id: id,
new_price: Price::new(1000),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_update_price_and_quantity_nonexistent() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let update = OrderUpdate::UpdatePriceAndQuantity {
order_id: id,
new_price: Price::new(1000),
new_quantity: Quantity::new(10),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_update_order_with_all_types() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id1 = create_order_id();
let timestamp = crate::utils::current_time_millis();
let trail_order = OrderType::TrailingStop {
id: id1,
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
trail_amount: Quantity::new(5),
last_reference_price: Price::new(995),
extra_fields: (),
};
let id2 = create_order_id();
let peg_order = OrderType::PeggedOrder {
id: id2,
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
reference_price_offset: 5,
reference_price_type: PegReferenceType::BestBid,
extra_fields: (),
};
let id3 = create_order_id();
let mtl_order = OrderType::MarketToLimit {
id: id3,
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let id4 = create_order_id();
let reserve_order = OrderType::ReserveOrder {
id: id4,
price: Price::new(1000),
visible_quantity: Quantity::new(5),
hidden_quantity: Quantity::new(5),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
replenish_threshold: Quantity::new(2),
replenish_amount: Some(std::num::NonZeroU64::new(3).expect("nonzero")),
auto_replenish: true,
extra_fields: (),
};
let _ = book.add_order(trail_order);
let _ = book.add_order(peg_order);
let _ = book.add_order(mtl_order);
let _ = book.add_order(reserve_order);
let update1 = OrderUpdate::UpdatePriceAndQuantity {
order_id: id1,
new_price: Price::new(1010),
new_quantity: Quantity::new(15),
};
let update2 = OrderUpdate::UpdatePriceAndQuantity {
order_id: id2,
new_price: Price::new(1010),
new_quantity: Quantity::new(15),
};
let update3 = OrderUpdate::UpdatePriceAndQuantity {
order_id: id3,
new_price: Price::new(1010),
new_quantity: Quantity::new(15),
};
let update4 = OrderUpdate::UpdatePriceAndQuantity {
order_id: id4,
new_price: Price::new(1010),
new_quantity: Quantity::new(15),
};
let result1 = book.update_order(update1);
let result2 = book.update_order(update2);
let result3 = book.update_order(update3);
let result4 = book.update_order(update4);
assert!(result1.is_ok());
assert!(result2.is_ok());
assert!(result3.is_ok());
assert!(result4.is_ok());
let order1 = book.get_order(id1);
let order2 = book.get_order(id2);
let order3 = book.get_order(id3);
let order4 = book.get_order(id4);
assert!(order1.is_some());
assert!(order2.is_some());
assert!(order3.is_some());
assert!(order4.is_some());
assert_eq!(order1.unwrap().price().as_u128(), 1010);
assert_eq!(order2.unwrap().price().as_u128(), 1010);
assert_eq!(order3.unwrap().price().as_u128(), 1010);
assert_eq!(order4.unwrap().price().as_u128(), 1010);
}
#[test]
fn test_replace_with_special_order_types() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let timestamp = crate::utils::current_time_millis();
let reserve_order = OrderType::ReserveOrder {
id,
price: Price::new(1000),
visible_quantity: Quantity::new(5),
hidden_quantity: Quantity::new(5),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
replenish_threshold: Quantity::new(2),
replenish_amount: Some(std::num::NonZeroU64::new(3).expect("nonzero")),
auto_replenish: true,
extra_fields: (),
};
let _ = book.add_order(reserve_order);
let update = OrderUpdate::Replace {
order_id: id,
price: Price::new(1010),
quantity: Quantity::new(15),
side: Side::Buy,
};
let result = book.update_order(update);
assert!(result.is_ok());
let updated_order = book.get_order(id);
assert!(updated_order.is_some());
assert_eq!(updated_order.clone().unwrap().price().as_u128(), 1010);
assert_eq!(updated_order.unwrap().visible_quantity(), Quantity::new(15));
}
#[test]
fn test_cancel_order_removes_price_level() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let _ = book.add_limit_order(id, 1000, 10, Side::Buy, TimeInForce::Gtc, None);
let update = OrderUpdate::Cancel { order_id: id };
let result = book.update_order(update);
assert!(result.is_ok());
assert_eq!(book.best_bid(), None);
assert!(book.get_order(id).is_none());
}
}
#[cfg(test)]
mod test_modifications_specific {
use crate::{OrderBook, OrderBookError};
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, PegReferenceType, Price, Quantity, Side, TimeInForce,
TimestampMs,
};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_update_price_edge_cases() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let update = OrderUpdate::UpdatePrice {
order_id: id,
new_price: Price::new(1000),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_cancel_non_existent_order() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let update = OrderUpdate::Cancel { order_id: id };
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_update_order_when_order_is_not_found() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let timestamp = crate::utils::current_time_millis();
let reserve_order = OrderType::ReserveOrder {
id,
price: Price::new(1000),
visible_quantity: Quantity::new(5),
hidden_quantity: Quantity::new(5),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
replenish_threshold: Quantity::new(2),
replenish_amount: Some(std::num::NonZeroU64::new(3).expect("nonzero")),
auto_replenish: true,
extra_fields: (),
};
let _ = book.add_order(reserve_order);
let nonexistent_id = create_order_id();
let update = OrderUpdate::UpdatePrice {
order_id: nonexistent_id,
new_price: Price::new(1100),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
let update = OrderUpdate::UpdateQuantity {
order_id: nonexistent_id,
new_quantity: Quantity::new(20),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
let update = OrderUpdate::UpdatePriceAndQuantity {
order_id: nonexistent_id,
new_price: Price::new(1100),
new_quantity: Quantity::new(20),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
let update = OrderUpdate::Replace {
order_id: nonexistent_id,
price: Price::new(1100),
quantity: Quantity::new(20),
side: Side::Buy,
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_replace_unsupported_order_type() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let timestamp = crate::utils::current_time_millis();
let peg_order = OrderType::PeggedOrder {
id,
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(timestamp),
time_in_force: TimeInForce::Gtc,
reference_price_offset: 5,
reference_price_type: PegReferenceType::BestBid,
extra_fields: (),
};
let _ = book.add_order(peg_order);
let update = OrderUpdate::Replace {
order_id: id,
price: Price::new(1100),
quantity: Quantity::new(20),
side: Side::Buy,
};
let result = book.update_order(update);
match result {
Err(OrderBookError::InvalidOperation { message }) => {
assert!(message.contains("Replace operation not supported"));
}
Ok(_) => {
let updated_order = book.get_order(id);
assert!(updated_order.is_some());
}
_ => panic!("Unexpected result"),
}
}
}
#[cfg(test)]
mod tests {
use crate::orderbook::OrderBookError;
use crate::orderbook::book::OrderBook;
use crate::orderbook::modifications::OrderQuantity;
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, Price, Quantity, Side, TimeInForce, TimestampMs,
};
fn setup_book_with_orders() -> OrderBook<()> {
let book: OrderBook<()> = OrderBook::new("TEST");
let sell_order = OrderType::Standard {
id: Id::new(),
side: Side::Sell,
price: Price::new(100),
quantity: Quantity::new(10),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(0),
extra_fields: (),
};
book.add_order(sell_order).unwrap();
let buy_order = OrderType::Standard {
id: Id::new(),
side: Side::Buy,
price: Price::new(90),
quantity: Quantity::new(10),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(0),
extra_fields: (),
};
book.add_order(buy_order).unwrap();
book
}
#[test]
fn test_add_post_only_order_crossing_market() {
let book = setup_book_with_orders();
let post_only_order = OrderType::PostOnly {
id: Id::new(),
side: Side::Buy,
price: Price::new(100), quantity: Quantity::new(5),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(0),
extra_fields: (),
};
let result = book.add_order(post_only_order);
assert!(matches!(result, Err(OrderBookError::PriceCrossing { .. })));
}
#[test]
fn test_add_expired_order() {
let book: OrderBook<()> = OrderBook::new("TEST");
book.set_market_close_timestamp(100);
let expired_order = OrderType::Standard {
id: Id::new(),
side: Side::Buy,
price: Price::new(95),
quantity: Quantity::new(10),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Day, timestamp: TimestampMs::new(101), extra_fields: (),
};
let result = book.add_order(expired_order);
assert!(matches!(
result,
Err(OrderBookError::InvalidOperation { .. })
));
}
#[test]
fn test_successful_cancel_order_removes_level() {
let book: OrderBook<()> = OrderBook::new("TEST");
let order_id = Id::new();
let order = OrderType::Standard {
id: order_id,
side: Side::Sell,
price: Price::new(100),
quantity: Quantity::new(10),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(0),
extra_fields: (),
};
book.add_order(order).unwrap();
assert!(book.asks.contains_key(&100));
book.cancel_order(order_id).unwrap();
assert!(!book.asks.contains_key(&100)); assert!(book.order_locations.get(&order_id).is_none());
}
#[test]
fn test_update_order_not_found() {
let book: OrderBook<()> = OrderBook::new("TEST");
let non_existent_id = Id::new();
let result = book.update_order(OrderUpdate::Cancel {
order_id: non_existent_id,
});
assert!(result.is_ok() && result.unwrap().is_none());
}
#[test]
fn test_update_price_and_quantity() {
let book = setup_book_with_orders();
let orders: Vec<_> = book.bids.get(&90).unwrap().value().iter_orders().collect();
let original_order_id = orders[0].id();
let result = book.update_order(OrderUpdate::UpdatePriceAndQuantity {
order_id: original_order_id,
new_price: Price::new(92),
new_quantity: Quantity::new(12),
});
assert!(result.is_ok());
let updated_order = book.get_order(original_order_id).unwrap();
assert_eq!(updated_order.price().as_u128(), 92);
assert_eq!(updated_order.visible_quantity(), Quantity::new(12));
assert!(book.bids.contains_key(&92));
assert!(!book.bids.contains_key(&90));
}
#[test]
fn test_set_quantity_for_reserve_order() {
let mut order = OrderType::ReserveOrder {
id: Id::new(),
side: Side::Buy,
price: Price::new(100),
visible_quantity: Quantity::new(10),
hidden_quantity: Quantity::new(90),
replenish_amount: Some(std::num::NonZeroU64::new(10).expect("nonzero")),
auto_replenish: true,
replenish_threshold: Quantity::new(0),
user_id: Hash32::zero(),
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(0),
extra_fields: (),
};
order.set_quantity(85);
assert_eq!(order.quantity(), 10); assert_eq!(order.total_quantity(), 85);
if let OrderType::ReserveOrder {
visible_quantity,
hidden_quantity,
..
} = order
{
assert_eq!(visible_quantity, Quantity::new(10));
assert_eq!(hidden_quantity, Quantity::new(75));
}
}
}
#[cfg(test)]
mod test_add_order_with_result {
use crate::orderbook::modifications::OrderQuantity;
use crate::orderbook::stp::STPMode;
use crate::{OrderBook, OrderBookError, TradeListener, TradeResult};
use pricelevel::{Hash32, Id, OrderType, Price, Quantity, Side, TimeInForce, TimestampMs};
use std::sync::{Arc, Mutex};
fn user(byte: u8) -> Hash32 {
Hash32::new([byte; 32])
}
fn standard_order(price: u128, quantity: u64, side: Side, user_id: Hash32) -> OrderType<()> {
OrderType::Standard {
id: Id::new(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side,
user_id,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
fn book_with_capturing_listener() -> (OrderBook<()>, Arc<Mutex<Vec<TradeResult>>>) {
let captured: Arc<Mutex<Vec<TradeResult>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&captured);
let listener: TradeListener = Arc::new(move |tr: &TradeResult| {
if let Ok(mut guard) = sink.lock() {
guard.push(tr.clone());
}
});
(OrderBook::with_trade_listener("TEST", listener), captured)
}
#[test]
fn test_add_order_with_result_full_fill_returns_trade_result() {
let book: OrderBook<()> = OrderBook::new("TEST");
let maker = user(1);
let taker = user(2);
let result = book.add_order(standard_order(100, 5, Side::Sell, maker));
assert!(result.is_ok(), "failed to rest maker: {result:?}");
let result = book.add_order_with_result(standard_order(100, 5, Side::Buy, taker));
let Ok((_, Some(trade_result))) = result else {
panic!("expected Ok with a trade result, got {result:?}");
};
assert_eq!(trade_result.symbol, "TEST");
assert_eq!(
trade_result.match_result.executed_quantity().ok(),
Some(Quantity::new(5)),
"full fill must execute the entire quantity"
);
assert!(trade_result.match_result.is_complete());
}
#[test]
fn test_add_order_with_result_resting_order_returns_none() {
let book: OrderBook<()> = OrderBook::new("TEST");
let order = standard_order(100, 10, Side::Buy, user(1));
let order_id = order.id();
let result = book.add_order_with_result(order);
let Ok((added, trade_result)) = result else {
panic!("expected Ok, got {result:?}");
};
assert!(
trade_result.is_none(),
"an order that matched nothing must return no trade result"
);
assert_eq!(added.id(), order_id);
assert!(book.get_order(order_id).is_some(), "order must rest");
}
#[test]
fn test_add_order_with_result_partial_fill_rests_remainder() {
let book: OrderBook<()> = OrderBook::new("TEST");
let maker = user(1);
let taker = user(2);
let result = book.add_order(standard_order(100, 5, Side::Sell, maker));
assert!(result.is_ok(), "failed to rest maker: {result:?}");
let result = book.add_order_with_result(standard_order(100, 20, Side::Buy, taker));
let Ok((added, Some(trade_result))) = result else {
panic!("expected Ok with a trade result, got {result:?}");
};
assert_eq!(
trade_result.match_result.executed_quantity().ok(),
Some(Quantity::new(5)),
"only the resting 5 units can fill"
);
assert_eq!(added.quantity(), 15, "the 15-unit remainder must rest");
assert!(book.get_order(added.id()).is_some());
}
#[test]
fn test_add_order_with_result_listener_receives_same_trade_result() {
let (book, captured) = book_with_capturing_listener();
let maker = user(1);
let taker = user(2);
let result = book.add_order(standard_order(100, 5, Side::Sell, maker));
assert!(result.is_ok(), "failed to rest maker: {result:?}");
let result = book.add_order_with_result(standard_order(100, 5, Side::Buy, taker));
let Ok((_, Some(trade_result))) = result else {
panic!("expected Ok with a trade result, got {result:?}");
};
let captured = captured.lock().expect("listener mutex poisoned");
assert_eq!(captured.len(), 1, "listener must fire exactly once");
assert_eq!(
captured[0].engine_seq, trade_result.engine_seq,
"listener and caller must see the same engine_seq"
);
assert_eq!(
captured[0].match_result.executed_quantity().ok(),
trade_result.match_result.executed_quantity().ok(),
"listener and caller must see the same fills"
);
}
#[test]
fn test_add_order_stp_cancel_taker_partial_fill_still_reaches_listener() {
let (mut book, captured) = book_with_capturing_listener();
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(7);
let other = user(1);
let result = book.add_order(standard_order(100, 5, Side::Sell, other));
assert!(result.is_ok(), "failed to rest other maker: {result:?}");
let result = book.add_order(standard_order(200, 10, Side::Sell, taker_user));
assert!(result.is_ok(), "failed to rest self maker: {result:?}");
let result = book.add_order(standard_order(200, 20, Side::Buy, taker_user));
assert!(
matches!(result, Err(OrderBookError::SelfTradePrevented { .. })),
"partial self-cross under CancelTaker must surface the STP error, got {result:?}"
);
let captured = captured.lock().expect("listener mutex poisoned");
assert_eq!(
captured.len(),
1,
"the 5-unit non-self fill must reach the trade listener"
);
assert_eq!(
captured[0].match_result.executed_quantity().ok(),
Some(Quantity::new(5))
);
}
#[test]
fn test_add_order_with_result_stp_cancel_taker_partial_fill_errors_and_emits() {
let (mut book, captured) = book_with_capturing_listener();
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(7);
let other = user(1);
let result = book.add_order(standard_order(100, 5, Side::Sell, other));
assert!(result.is_ok(), "failed to rest other maker: {result:?}");
let result = book.add_order(standard_order(200, 10, Side::Sell, taker_user));
assert!(result.is_ok(), "failed to rest self maker: {result:?}");
let result = book.add_order_with_result(standard_order(200, 20, Side::Buy, taker_user));
assert!(
matches!(result, Err(OrderBookError::SelfTradePrevented { .. })),
"partial self-cross under CancelTaker must surface the STP error, got {result:?}"
);
let captured = captured.lock().expect("listener mutex poisoned");
assert_eq!(
captured.len(),
1,
"the 5-unit non-self fill must reach the trade listener"
);
}
#[test]
fn test_add_order_with_result_ioc_partial_fill_errors_and_emits() {
let (book, captured) = book_with_capturing_listener();
let maker = user(1);
let taker = user(2);
let result = book.add_order(standard_order(100, 5, Side::Sell, maker));
assert!(result.is_ok(), "failed to rest maker: {result:?}");
let ioc = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(20),
side: Side::Buy,
user_id: taker,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Ioc,
extra_fields: (),
};
let result = book.add_order_with_result(ioc);
assert!(
matches!(result, Err(OrderBookError::InsufficientLiquidity { .. })),
"unfillable IOC remainder must error, got {result:?}"
);
let captured = captured.lock().expect("listener mutex poisoned");
assert_eq!(
captured.len(),
1,
"the 5-unit partial fill must reach the trade listener"
);
assert_eq!(
captured[0].match_result.executed_quantity().ok(),
Some(Quantity::new(5))
);
}
#[test]
fn test_add_order_with_result_concurrent_per_call_attribution() {
use std::sync::Barrier;
use std::thread;
const NUM_TAKERS: usize = 4;
const PER_TAKER: u64 = 5;
const RESTING_TOTAL: u64 = NUM_TAKERS as u64 * PER_TAKER;
const ITERATIONS: usize = 250;
for iteration in 0..ITERATIONS {
let book: OrderBook<()> = OrderBook::new("TEST");
let seed = standard_order(100, RESTING_TOTAL, Side::Sell, user(1));
assert!(
book.add_order(seed).is_ok(),
"iteration {iteration}: failed to seed resting liquidity"
);
let barrier = Barrier::new(NUM_TAKERS);
let mut combined_executed: u64 = 0;
thread::scope(|scope| {
let handles: Vec<_> = (0..NUM_TAKERS)
.map(|i| {
let book = &book;
let barrier = &barrier;
scope.spawn(move || {
let taker = user(10 + i as u8);
let order = standard_order(100, PER_TAKER, Side::Buy, taker);
let submitted_id = order.id();
barrier.wait();
let result = book.add_order_with_result(order);
(submitted_id, result)
})
})
.collect();
for handle in handles {
let (submitted_id, result) = handle.join().expect("taker thread panicked");
let Ok((added, Some(trade_result))) = result else {
panic!("iteration {iteration}: expected a filled taker, got {result:?}");
};
assert_eq!(
added.id(),
submitted_id,
"iteration {iteration}: returned order id must be the submitted id"
);
assert_eq!(
trade_result.match_result.order_id(),
submitted_id,
"iteration {iteration}: TradeResult must attribute to the submitting call"
);
let per_trade_sum: u64 = trade_result
.match_result
.trades()
.as_vec()
.iter()
.map(|t| t.quantity().as_u64())
.sum();
let executed = trade_result
.match_result
.executed_quantity()
.expect("executed quantity")
.as_u64();
assert_eq!(
per_trade_sum, executed,
"iteration {iteration}: per-trade fills must sum to executed quantity"
);
assert_eq!(
executed, PER_TAKER,
"iteration {iteration}: each taker must fully fill its own size"
);
combined_executed += executed;
}
});
assert_eq!(
combined_executed, RESTING_TOTAL,
"iteration {iteration}: combined fills must equal consumed resting size"
);
assert!(
book.best_ask().is_none(),
"iteration {iteration}: all resting liquidity must have been consumed"
);
}
}
}