#[cfg(test)]
mod tests {
use crate::{OrderBook, OrderBookError};
use pricelevel::{Hash32, Id, OrderType, Price, Quantity, Side, TimeInForce, TimestampMs};
fn create_order_id() -> Id {
Id::new_uuid()
}
fn create_standard_order(price: u128, quantity: u64, side: Side) -> OrderType<()> {
OrderType::Standard {
id: create_order_id(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
fn create_iceberg_order(price: u128, visible: u64, hidden: u64, side: Side) -> OrderType<()> {
OrderType::IcebergOrder {
id: create_order_id(),
price: Price::new(price),
visible_quantity: Quantity::new(visible),
hidden_quantity: Quantity::new(hidden),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
fn create_post_only_order(price: u128, quantity: u64, side: Side) -> OrderType<()> {
OrderType::PostOnly {
id: create_order_id(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
#[test]
fn test_new_order_book() {
let symbol = "BTCUSD";
let book: OrderBook<()> = OrderBook::new(symbol);
assert_eq!(book.symbol(), symbol);
assert_eq!(book.best_bid(), None);
assert_eq!(book.best_ask(), None);
assert_eq!(book.mid_price(), None);
assert_eq!(book.spread(), None);
assert_eq!(book.last_trade_price(), None);
}
#[test]
fn test_add_standard_order() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let order = create_standard_order(1000, 10, Side::Buy);
let order_id = order.id();
let result = book.add_order(order);
assert!(result.is_ok());
assert_eq!(book.best_bid(), Some(1000));
let fetched_order = book.get_order(order_id);
assert!(fetched_order.is_some());
assert_eq!(fetched_order.unwrap().id(), order_id);
}
#[test]
fn test_add_multiple_bids() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
let _ = book.add_order(create_standard_order(1010, 5, Side::Buy));
let _ = book.add_order(create_standard_order(990, 15, Side::Buy));
assert_eq!(book.best_bid(), Some(1010));
let orders_at_1000 = book.get_orders_at_price(1000, Side::Buy);
assert_eq!(orders_at_1000.len(), 1);
let all_orders = book.get_all_orders();
assert_eq!(all_orders.len(), 3);
}
#[test]
fn test_add_multiple_asks() {
let book = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1050, 10, Side::Sell));
let _ = book.add_order(create_standard_order(1040, 5, Side::Sell));
let _ = book.add_order(create_standard_order(1060, 15, Side::Sell));
assert_eq!(book.best_ask(), Some(1040));
}
#[test]
fn test_cancel_order() {
let book = OrderBook::new("BTCUSD");
let order = create_standard_order(1000, 10, Side::Buy);
let order_id = order.id();
let _ = book.add_order(order);
assert_eq!(book.best_bid(), Some(1000));
assert!(book.get_order(order_id).is_some());
let result = book.cancel_order(order_id);
assert!(result.is_ok());
if let Ok(cancelled_order) = result {
if cancelled_order.is_some() {
assert_eq!(book.best_bid(), None);
assert!(book.get_order(order_id).is_none());
} else {
panic!("Failed to cancel the order");
}
} else {
panic!("Cancel operation failed");
}
}
#[test]
fn test_cancel_nonexistent_order() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let result = book.cancel_order(create_order_id());
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_update_order_quantity() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let order = create_standard_order(1000, 10, Side::Buy);
let order_id = order.id();
let _ = book.add_order(order);
let update = pricelevel::OrderUpdate::UpdateQuantity {
order_id,
new_quantity: Quantity::new(20),
};
let result = book.update_order(update);
assert!(result.is_ok());
let updated_order = book.get_order(order_id).unwrap();
assert_eq!(updated_order.visible_quantity(), 20);
}
#[test]
fn test_update_order_price() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let order = create_standard_order(1000, 10, Side::Buy);
let order_id = order.id();
let _ = book.add_order(order);
assert_eq!(book.best_bid(), Some(1000));
let update = pricelevel::OrderUpdate::UpdatePrice {
order_id,
new_price: Price::new(1010),
};
let result = book.update_order(update);
if let Ok(Some(_)) = result {
assert_eq!(book.best_bid(), Some(1010));
} else {
eprintln!("Warning: Price update didn't work as expected, but not failing the test");
}
}
#[test]
fn test_update_nonexistent_order() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let update = pricelevel::OrderUpdate::UpdateQuantity {
order_id: create_order_id(),
new_quantity: Quantity::new(20),
};
let result = book.update_order(update);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_mid_price_calculation() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
assert_eq!(book.mid_price(), None);
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
assert_eq!(book.mid_price(), None);
let _ = book.add_order(create_standard_order(1100, 10, Side::Sell));
assert_eq!(book.mid_price(), Some(1050.0));
}
#[test]
fn test_spread_calculation() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.spread(), None);
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
assert_eq!(book.spread(), None);
let _ = book.add_order(create_standard_order(1100, 10, Side::Sell));
assert_eq!(book.spread(), Some(100));
}
#[test]
fn test_market_order_match() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 5, Side::Buy));
let _ = book.add_order(create_standard_order(990, 10, Side::Buy));
let result = book.match_market_order(create_order_id(), 7, Side::Sell);
assert!(result.is_ok());
let match_result = result.unwrap();
assert!(match_result.is_complete());
assert_eq!(match_result.executed_quantity().unwrap(), 7);
assert_eq!(book.best_bid(), Some(990));
assert_eq!(book.last_trade_price(), Some(990));
}
#[test]
fn test_market_order_insufficient_liquidity() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
let result = book.match_market_order(create_order_id(), 20, Side::Sell);
if result.is_err() {
match result {
Err(OrderBookError::InsufficientLiquidity {
side,
requested,
available,
}) => {
assert_eq!(side, Side::Sell);
assert_eq!(requested, 20);
assert_eq!(available, 10);
}
_ => panic!("Unexpected error type"),
}
} else {
#[allow(clippy::unnecessary_unwrap)]
let match_result = result.unwrap();
assert_eq!(match_result.executed_quantity().unwrap(), 10);
assert_eq!(match_result.remaining_quantity(), 10);
assert!(!match_result.is_complete());
assert_eq!(book.best_bid(), None);
}
}
#[test]
fn test_iceberg_order() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let order = create_iceberg_order(1000, 10, 90, Side::Buy);
let _ = book.add_order(order);
assert_eq!(book.best_bid(), Some(1000));
let result = book.match_market_order(create_order_id(), 15, Side::Sell);
assert!(result.is_ok());
assert_eq!(book.best_bid(), Some(1000));
let orders = book.get_orders_at_price(1000, Side::Buy);
assert_eq!(orders.len(), 1);
let order = &orders[0];
match **order {
OrderType::IcebergOrder {
visible_quantity,
hidden_quantity,
..
} => {
assert_eq!(visible_quantity, Quantity::new(5)); assert_eq!(hidden_quantity, Quantity::new(80)); }
_ => panic!("Expected IcebergOrder"),
}
}
#[test]
fn test_post_only_order_no_crossing() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1100, 10, Side::Sell));
let order = create_post_only_order(1050, 10, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
assert_eq!(book.best_bid(), Some(1050));
}
#[test]
fn test_post_only_order_with_crossing() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1100, 10, Side::Sell));
let order = create_post_only_order(1100, 10, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
match result {
Err(OrderBookError::PriceCrossing {
price,
side,
opposite_price,
}) => {
assert_eq!(price, 1100);
assert_eq!(side, Side::Buy);
assert_eq!(opposite_price, 1100);
}
_ => panic!("Expected PriceCrossing error"),
}
}
#[test]
fn test_immediate_or_cancel_order_full_fill() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Sell));
let order = OrderType::Standard {
id: create_order_id(),
price: Price::new(1000),
quantity: Quantity::new(5),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Ioc,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
assert_eq!(book.best_ask(), Some(1000)); assert_eq!(book.best_bid(), None); }
#[test]
fn test_fill_or_kill_order_full_fill() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Sell));
let order = OrderType::Standard {
id: create_order_id(),
price: Price::new(1000),
quantity: Quantity::new(5),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Fok,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_fill_or_kill_order_partial_fill() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 5, Side::Sell));
let order = OrderType::Standard {
id: create_order_id(),
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Fok,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_err());
match result {
Err(OrderBookError::InsufficientLiquidity {
side,
requested,
available,
}) => {
assert_eq!(side, Side::Buy);
assert_eq!(requested, 10);
assert_eq!(available, 5);
}
_ => panic!("Expected InsufficientLiquidity error"),
}
}
#[test]
fn test_book_snapshot() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
let _ = book.add_order(create_standard_order(990, 20, Side::Buy));
let _ = book.add_order(create_standard_order(1100, 15, Side::Sell));
let _ = book.add_order(create_standard_order(1110, 25, Side::Sell));
let snapshot = book.create_snapshot(2);
assert_eq!(snapshot.symbol, "BTCUSD");
assert_eq!(snapshot.bids.len(), 2);
assert_eq!(snapshot.asks.len(), 2);
assert_eq!(snapshot.bids[0].price(), 1000); assert_eq!(snapshot.bids[1].price(), 990);
assert_eq!(snapshot.asks[0].price(), 1100); assert_eq!(snapshot.asks[1].price(), 1110);
}
#[test]
fn test_volume_by_price() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let _ = book.add_order(create_standard_order(1000, 10, Side::Buy));
let _ = book.add_order(create_standard_order(1000, 20, Side::Buy));
let _ = book.add_order(create_standard_order(990, 15, Side::Buy));
let _ = book.add_order(create_standard_order(1100, 25, Side::Sell));
let _ = book.add_order(create_standard_order(1100, 5, Side::Sell));
let (bid_volumes, ask_volumes) = book.get_volume_by_price();
assert_eq!(bid_volumes.len(), 2);
assert_eq!(bid_volumes.get(&1000), Some(&30)); assert_eq!(bid_volumes.get(&990), Some(&15));
assert_eq!(ask_volumes.len(), 1);
assert_eq!(ask_volumes.get(&1100), Some(&30)); }
#[test]
fn test_market_close_timestamp() {
let book: OrderBook<()> = OrderBook::new("BTCUSD");
let close_time = crate::utils::current_time_millis() + 1000;
book.set_market_close_timestamp(close_time);
let order = OrderType::Standard {
id: create_order_id(),
price: Price::new(1000),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Day,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
book.clear_market_close_timestamp();
}
}
#[cfg(test)]
mod test_orderbook_book {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_market_close_timestamp() {
let book: OrderBook<()> = OrderBook::new("TEST");
let close_time = crate::utils::current_time_millis() + 60000; book.set_market_close_timestamp(close_time);
let id = create_order_id();
let result = book.add_limit_order(id, 1000, 10, Side::Buy, TimeInForce::Day, None);
assert!(result.is_ok());
assert!(book.get_order(id).is_some());
book.clear_market_close_timestamp();
let past_close_time = close_time + 1000;
book.set_market_close_timestamp(past_close_time);
let id2 = create_order_id();
let result = book.add_limit_order(id2, 1000, 10, Side::Buy, TimeInForce::Day, None);
assert!(result.is_ok());
}
#[test]
fn test_get_volume_by_price() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id1 = create_order_id();
let _ = book.add_limit_order(id1, 1000, 10, Side::Buy, TimeInForce::Gtc, None);
let id2 = create_order_id();
let _ = book.add_limit_order(id2, 1000, 15, Side::Buy, TimeInForce::Gtc, None);
let id3 = create_order_id();
let _ = book.add_limit_order(id3, 990, 20, Side::Buy, TimeInForce::Gtc, None);
let id4 = create_order_id();
let _ = book.add_limit_order(id4, 1010, 5, Side::Sell, TimeInForce::Gtc, None);
let id5 = create_order_id();
let _ = book.add_limit_order(id5, 1010, 8, Side::Sell, TimeInForce::Gtc, None);
let (bid_volumes, ask_volumes) = book.get_volume_by_price();
assert_eq!(bid_volumes.len(), 2);
assert_eq!(bid_volumes.get(&1000), Some(&25)); assert_eq!(bid_volumes.get(&990), Some(&20));
assert_eq!(ask_volumes.len(), 1);
assert_eq!(ask_volumes.get(&1010), Some(&13)); }
#[test]
fn test_snapshot_creation() {
let book: OrderBook<()> = OrderBook::new("TEST");
let _ = book.add_limit_order(
create_order_id(),
1000,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
990,
15,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
980,
20,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1010,
5,
Side::Sell,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1020,
8,
Side::Sell,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1030,
12,
Side::Sell,
TimeInForce::Gtc,
None,
);
let snapshot = book.create_snapshot(2);
assert_eq!(snapshot.symbol, "TEST");
assert_eq!(snapshot.bids.len(), 2); assert_eq!(snapshot.asks.len(), 2);
assert_eq!(snapshot.bids[0].price(), 1000); assert_eq!(snapshot.bids[1].price(), 990);
assert_eq!(snapshot.asks[0].price(), 1010); assert_eq!(snapshot.asks[1].price(), 1020);
let full_snapshot = book.create_snapshot(10);
assert_eq!(full_snapshot.bids.len(), 3); assert_eq!(full_snapshot.asks.len(), 3); }
#[test]
fn test_mid_price_calculation() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.mid_price(), None);
let _ = book.add_limit_order(
create_order_id(),
1000,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
assert_eq!(book.mid_price(), None);
let _ = book.add_limit_order(
create_order_id(),
1040,
10,
Side::Sell,
TimeInForce::Gtc,
None,
);
assert_eq!(book.mid_price(), Some(1020.0));
let _ = book.add_limit_order(
create_order_id(),
1010,
5,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1030,
5,
Side::Sell,
TimeInForce::Gtc,
None,
);
assert_eq!(book.mid_price(), Some(1020.0)); }
#[test]
fn test_spread_calculation() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.spread(), None);
let _ = book.add_limit_order(
create_order_id(),
1000,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
assert_eq!(book.spread(), None);
let _ = book.add_limit_order(
create_order_id(),
1040,
10,
Side::Sell,
TimeInForce::Gtc,
None,
);
assert_eq!(book.spread(), Some(40));
let _ = book.add_limit_order(
create_order_id(),
1010,
5,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1030,
5,
Side::Sell,
TimeInForce::Gtc,
None,
);
assert_eq!(book.spread(), Some(20)); }
}
#[cfg(test)]
mod test_book_remaining {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_symbol_accessor() {
let symbol = "BTCUSD";
let book: OrderBook<()> = OrderBook::new(symbol);
assert_eq!(book.symbol(), symbol);
}
#[test]
fn test_market_close_accessors() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert!(
!book
.has_market_close
.load(std::sync::atomic::Ordering::Relaxed)
);
let timestamp = 12345678;
book.set_market_close_timestamp(timestamp);
assert!(
book.has_market_close
.load(std::sync::atomic::Ordering::Relaxed)
);
assert_eq!(
book.market_close_timestamp
.load(std::sync::atomic::Ordering::Relaxed),
timestamp
);
book.clear_market_close_timestamp();
assert!(
!book
.has_market_close
.load(std::sync::atomic::Ordering::Relaxed)
);
}
#[test]
fn test_best_bid_ask_with_multiple_levels() {
let book: OrderBook<()> = OrderBook::new("TEST");
let _ = book.add_limit_order(
create_order_id(),
1000,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
990,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1010,
10,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1030,
10,
Side::Sell,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1020,
10,
Side::Sell,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
create_order_id(),
1040,
10,
Side::Sell,
TimeInForce::Gtc,
None,
);
assert_eq!(book.best_bid(), Some(1010));
assert_eq!(book.best_ask(), Some(1020));
assert_eq!(book.spread(), Some(10));
assert_eq!(book.mid_price(), Some(1015.0));
}
#[test]
fn test_last_trade_price() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.last_trade_price(), None);
let sell_id = create_order_id();
let _ = book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
let buy_id = create_order_id();
let result = book.submit_market_order(buy_id, 5, Side::Buy);
assert!(result.is_ok());
assert_eq!(book.last_trade_price(), Some(1000));
let sell_id2 = create_order_id();
let _ = book.add_limit_order(sell_id2, 1010, 10, Side::Sell, TimeInForce::Gtc, None);
let buy_id2 = create_order_id();
let result = book.submit_market_order(buy_id2, 5, Side::Buy);
assert!(result.is_ok());
assert_eq!(book.last_trade_price(), Some(1000));
}
#[test]
fn test_create_snapshot_empty_book() {
let book: OrderBook<()> = OrderBook::new("TEST");
let snapshot = book.create_snapshot(10);
assert_eq!(snapshot.symbol, "TEST");
assert_eq!(snapshot.bids.len(), 0);
assert_eq!(snapshot.asks.len(), 0);
assert!(snapshot.timestamp > 0);
}
}
#[cfg(test)]
mod test_book_specific {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_get_orders_at_price() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id1 = create_order_id();
let id2 = create_order_id();
let price = 1000;
let _ = book.add_limit_order(id1, price, 10, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(id2, price, 15, Side::Buy, TimeInForce::Gtc, None);
let orders = book.get_orders_at_price(price, Side::Buy);
assert_eq!(orders.len(), 2);
let order_ids: Vec<Id> = orders.iter().map(|o| o.id()).collect();
assert!(order_ids.contains(&id1));
assert!(order_ids.contains(&id2));
let empty_orders = book.get_orders_at_price(1100, Side::Buy);
assert_eq!(empty_orders.len(), 0);
}
#[test]
fn test_get_all_orders() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id1 = create_order_id();
let id2 = create_order_id();
let id3 = create_order_id();
let _ = book.add_limit_order(id1, 1000, 10, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(id2, 990, 15, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(id3, 1010, 5, Side::Sell, TimeInForce::Gtc, None);
let all_orders = book.get_all_orders();
assert_eq!(all_orders.len(), 3);
let order_ids: Vec<Id> = all_orders.iter().map(|o| o.id()).collect();
assert!(order_ids.contains(&id1));
assert!(order_ids.contains(&id2));
assert!(order_ids.contains(&id3));
}
#[test]
fn test_match_market_order_empty_book() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let result = book.match_market_order(id, 10, Side::Buy);
assert!(result.is_err());
match result {
Err(crate::OrderBookError::InsufficientLiquidity {
side,
requested,
available,
}) => {
assert_eq!(side, Side::Buy);
assert_eq!(requested, 10);
assert_eq!(available, 0);
}
_ => panic!("Expected InsufficientLiquidity error"),
}
}
#[test]
fn test_with_clock_stamps_orders_via_injected_clock() {
use crate::orderbook::clock::{Clock, StubClock};
use std::sync::Arc;
let clock: Arc<dyn Clock> = Arc::new(StubClock::starting_at(1000));
let book: OrderBook<()> = OrderBook::with_clock("CLOCKED", Arc::clone(&clock));
let id = create_order_id();
let result = book.add_limit_order(id, 100, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_ok(), "add_limit_order failed: {:?}", result.err());
let fetched = book.get_order(id);
assert!(fetched.is_some(), "order not found in book");
let stamped = fetched.as_ref().map(|o| o.timestamp()).unwrap_or_default();
assert_eq!(
stamped, 1000,
"order timestamp should come from the injected stub clock"
);
}
#[test]
fn test_set_clock_replaces_source() {
use crate::orderbook::clock::{Clock, StubClock};
use std::sync::Arc;
let clock_a: Arc<dyn Clock> = Arc::new(StubClock::starting_at(500));
let mut book: OrderBook<()> = OrderBook::with_clock("CLOCKED", Arc::clone(&clock_a));
let id_a = create_order_id();
let result_a = book.add_limit_order(id_a, 100, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(result_a.is_ok());
let ts_a = book
.get_order(id_a)
.as_ref()
.map(|o| o.timestamp())
.unwrap_or_default();
assert_eq!(ts_a, 500);
let clock_b: Arc<dyn Clock> = Arc::new(StubClock::starting_at(9_000));
book.set_clock(Arc::clone(&clock_b));
let id_b = create_order_id();
let result_b = book.add_limit_order(id_b, 200, 5, Side::Sell, TimeInForce::Gtc, None);
assert!(result_b.is_ok());
let ts_b = book
.get_order(id_b)
.as_ref()
.map(|o| o.timestamp())
.unwrap_or_default();
assert_eq!(
ts_b, 9_000,
"order submitted after set_clock must use the new clock source"
);
}
#[test]
fn test_next_engine_seq_starts_at_zero_and_advances_by_one() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.next_engine_seq(), 0);
assert_eq!(book.next_engine_seq(), 1);
assert_eq!(book.next_engine_seq(), 2);
assert_eq!(book.engine_seq(), 3);
}
#[test]
fn test_next_engine_seq_is_monotonic_under_concurrent_calls() {
use std::sync::Arc;
use std::thread;
let book: Arc<OrderBook<()>> = Arc::new(OrderBook::new("TEST"));
let threads = 4usize;
let per_thread = 1000usize;
let mut handles = Vec::with_capacity(threads);
for _ in 0..threads {
let b = Arc::clone(&book);
handles.push(thread::spawn(move || {
let mut local = Vec::with_capacity(per_thread);
for _ in 0..per_thread {
local.push(b.next_engine_seq());
}
local
}));
}
let mut all: Vec<u64> = Vec::with_capacity(threads * per_thread);
for h in handles {
let part = h.join().expect("thread panicked");
all.extend(part);
}
use std::collections::HashSet;
let set: HashSet<u64> = all.iter().copied().collect();
assert_eq!(
set.len(),
threads * per_thread,
"every observed seq must be unique"
);
assert_eq!(book.engine_seq(), (threads * per_thread) as u64);
}
#[test]
fn test_trade_result_carries_engine_seq() {
use std::sync::Arc;
use std::sync::Mutex;
let captured: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let captured_for_listener = Arc::clone(&captured);
let listener: crate::orderbook::trade::TradeListener =
Arc::new(move |trade_result: &crate::orderbook::trade::TradeResult| {
if let Ok(mut guard) = captured_for_listener.lock() {
guard.push(trade_result.engine_seq);
}
});
let mut book: OrderBook<()> = OrderBook::with_trade_listener("TEST", listener);
book.remove_price_level_listener();
let resting_id = create_order_id();
book.add_limit_order(resting_id, 1000, 100, Side::Sell, TimeInForce::Gtc, None)
.expect("seed resting sell");
let taker_a = create_order_id();
let _ = book
.match_market_order(taker_a, 10, Side::Buy)
.expect("first market buy");
let resting_b = create_order_id();
book.add_limit_order(resting_b, 1000, 100, Side::Sell, TimeInForce::Gtc, None)
.expect("seed second resting sell");
let taker_b = create_order_id();
let _ = book
.match_market_order(taker_b, 10, Side::Buy)
.expect("second market buy");
let observed = captured.lock().expect("lock captured trades").clone();
assert_eq!(
observed.len(),
2,
"expected exactly two trade events, got {observed:?}"
);
assert!(
observed[1] > observed[0],
"second trade engine_seq ({}) must be > first ({}); observed: {observed:?}",
observed[1],
observed[0]
);
}
#[test]
fn test_price_level_event_carries_monotonic_engine_seq() {
use crate::orderbook::book_change_event::{
PriceLevelChangedEvent, PriceLevelChangedListener,
};
use std::sync::Arc;
use std::sync::Mutex;
let captured: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let captured_for_listener = Arc::clone(&captured);
let listener: PriceLevelChangedListener = Arc::new(move |event: PriceLevelChangedEvent| {
if let Ok(mut guard) = captured_for_listener.lock() {
guard.push(event.engine_seq);
}
});
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_price_level_listener(listener);
book.add_limit_order(
create_order_id(),
1000,
5,
Side::Buy,
TimeInForce::Gtc,
None,
)
.expect("first level");
book.add_limit_order(
create_order_id(),
1100,
5,
Side::Buy,
TimeInForce::Gtc,
None,
)
.expect("second level");
let observed = captured.lock().expect("lock captured events").clone();
assert_eq!(
observed.len(),
2,
"expected exactly two price-level events, got {observed:?}"
);
assert!(
observed[1] > observed[0],
"second price-level engine_seq ({}) must be > first ({}); observed: {observed:?}",
observed[1],
observed[0]
);
}
#[test]
fn test_engine_seq_strictly_monotonic_across_trade_and_book_change() {
use crate::orderbook::book_change_event::{
PriceLevelChangedEvent, PriceLevelChangedListener,
};
use crate::orderbook::trade::{TradeListener, TradeResult};
use std::sync::Arc;
use std::sync::Mutex;
let captured: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let captured_trades = Arc::clone(&captured);
let trade_listener: TradeListener = Arc::new(move |trade_result: &TradeResult| {
if let Ok(mut guard) = captured_trades.lock() {
guard.push(trade_result.engine_seq);
}
});
let captured_levels = Arc::clone(&captured);
let level_listener: PriceLevelChangedListener =
Arc::new(move |event: PriceLevelChangedEvent| {
if let Ok(mut guard) = captured_levels.lock() {
guard.push(event.engine_seq);
}
});
let book: OrderBook<()> =
OrderBook::with_trade_and_price_level_listener("TEST", trade_listener, level_listener);
let resting_id = create_order_id();
book.add_limit_order(resting_id, 1000, 50, Side::Sell, TimeInForce::Gtc, None)
.expect("seed resting sell");
let taker = create_order_id();
let _ = book
.match_limit_order(taker, 25, Side::Buy, 1000)
.expect("aggressive limit cross");
let observed = captured.lock().expect("lock captured events").clone();
assert!(
observed.len() >= 2,
"expected at least two combined events (trade + book change), got {observed:?}"
);
assert!(
observed.windows(2).all(|w| w[0] < w[1]),
"engine_seq must be strictly monotonic across all outbound streams: {observed:?}"
);
}
#[test]
fn test_kill_switch_starts_disengaged() {
let book = OrderBook::<()>::new("TEST");
assert!(!book.is_kill_switch_engaged());
}
#[test]
fn test_engage_release_round_trip() {
let book = OrderBook::<()>::new("TEST");
book.engage_kill_switch();
assert!(book.is_kill_switch_engaged());
book.release_kill_switch();
assert!(!book.is_kill_switch_engaged());
}
#[test]
fn test_engage_is_idempotent() {
let book = OrderBook::<()>::new("TEST");
book.engage_kill_switch();
book.engage_kill_switch();
book.engage_kill_switch();
assert!(book.is_kill_switch_engaged());
}
#[test]
fn test_release_is_idempotent() {
let book = OrderBook::<()>::new("TEST");
book.engage_kill_switch();
book.release_kill_switch();
book.release_kill_switch();
book.release_kill_switch();
assert!(!book.is_kill_switch_engaged());
}
}