#[cfg(test)]
mod tests {
use crate::orderbook::book::OrderBook;
use crate::orderbook::error::OrderBookError;
use crate::orderbook::stp::STPMode;
use pricelevel::{Hash32, Id, OrderType, Price, Quantity, Side, TimeInForce, TimestampMs};
fn user(byte: u8) -> Hash32 {
Hash32::new([byte; 32])
}
fn add_sell_order_with_user(
book: &OrderBook<()>,
price: u128,
quantity: u64,
user_id: Hash32,
) -> Id {
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side: Side::Sell,
user_id,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let id = order.id();
let result = book.add_order(order);
assert!(result.is_ok(), "failed to add sell order: {result:?}");
id
}
fn add_buy_order_with_user(
book: &OrderBook<()>,
price: u128,
quantity: u64,
user_id: Hash32,
) -> Id {
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side: Side::Buy,
user_id,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let id = order.id();
let result = book.add_order(order);
assert!(result.is_ok(), "failed to add buy order: {result:?}");
id
}
#[test]
fn test_stp_none_orders_match_normally() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.stp_mode(), STPMode::None);
let same_user = user(1);
add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert!(mr.is_complete());
assert_eq!(mr.executed_quantity().unwrap(), 10);
}
#[test]
fn test_cancel_taker_prevents_self_trade() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
let maker_id = add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelTaker);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
assert!(book.get_order(maker_id).is_some());
assert_eq!(book.best_ask(), Some(100));
}
#[test]
fn test_cancel_taker_allows_different_users() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let maker_user = user(1);
let taker_user = user(2);
add_sell_order_with_user(&book, 100, 10, maker_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, taker_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert!(mr.is_complete());
assert_eq!(mr.executed_quantity().unwrap(), 10);
}
#[test]
fn test_cancel_taker_partial_fill_before_self_trade() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(1);
let other_user = user(2);
add_sell_order_with_user(&book, 100, 5, other_user);
add_sell_order_with_user(&book, 200, 10, taker_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);
assert!(result.is_ok(), "expected Ok, got: {result:?}");
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 5);
assert!(!mr.is_complete());
}
#[test]
fn test_cancel_taker_zero_taker_user_bypasses_stp_during_matching() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let maker_user = user(1);
add_sell_order_with_user(&book, 100, 10, maker_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, Hash32::zero());
assert!(result.is_ok());
let mr = result.unwrap();
assert!(mr.is_complete());
}
#[test]
fn test_cancel_maker_removes_same_user_orders() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let same_user = user(1);
let other_user = user(2);
let maker_id = add_sell_order_with_user(&book, 100, 5, same_user);
add_sell_order_with_user(&book, 100, 10, other_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 10);
assert!(mr.is_complete());
assert!(book.get_order(maker_id).is_none());
}
#[test]
fn test_cancel_maker_all_same_user_orders_cancelled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let same_user = user(1);
let maker_id1 = add_sell_order_with_user(&book, 100, 5, same_user);
let maker_id2 = add_sell_order_with_user(&book, 100, 3, same_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
match result {
Err(OrderBookError::InsufficientLiquidity { .. }) => {}
other => panic!("expected InsufficientLiquidity, got {other:?}"),
}
assert!(book.get_order(maker_id1).is_none());
assert!(book.get_order(maker_id2).is_none());
assert_eq!(book.best_ask(), None);
}
#[test]
fn test_cancel_maker_across_price_levels() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let same_user = user(1);
let other_user = user(2);
let maker1 = add_sell_order_with_user(&book, 100, 5, same_user);
add_sell_order_with_user(&book, 200, 10, other_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 10);
assert!(book.get_order(maker1).is_none());
}
#[test]
fn test_cancel_both_cancels_maker_and_taker() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let same_user = user(1);
let maker_id = add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, same_user);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelBoth);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
assert!(book.get_order(maker_id).is_none());
}
#[test]
fn test_cancel_both_partial_fill_before_self() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let taker_user = user(1);
let other_user = user(2);
add_sell_order_with_user(&book, 100, 3, other_user);
let maker_id = add_sell_order_with_user(&book, 200, 10, taker_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 3);
assert!(!mr.is_complete());
assert!(book.get_order(maker_id).is_none());
}
#[test]
fn test_stp_cancel_taker_via_add_order() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
add_sell_order_with_user(&book, 100, 10, same_user);
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: same_user,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelTaker);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
assert_eq!(book.best_ask(), Some(100));
}
#[test]
fn test_stp_cancel_maker_via_add_order() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let same_user = user(1);
let other_user = user(2);
let maker_id = add_sell_order_with_user(&book, 100, 5, same_user);
add_sell_order_with_user(&book, 100, 10, other_user);
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(8),
side: Side::Buy,
user_id: same_user,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
assert!(book.get_order(maker_id).is_none());
}
#[test]
fn test_stp_cancel_taker_sell_side() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
let maker_id = add_buy_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Sell, same_user);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelTaker);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
assert!(book.get_order(maker_id).is_some());
assert_eq!(book.best_bid(), Some(100));
}
#[test]
fn test_stp_cancel_maker_sell_side() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let same_user = user(1);
let other_user = user(2);
let maker_id = add_buy_order_with_user(&book, 200, 5, same_user);
add_buy_order_with_user(&book, 200, 10, other_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Sell, same_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 10);
assert!(book.get_order(maker_id).is_none());
}
#[test]
fn test_stp_mode_setter_getter() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.stp_mode(), STPMode::None);
book.set_stp_mode(STPMode::CancelTaker);
assert_eq!(book.stp_mode(), STPMode::CancelTaker);
book.set_stp_mode(STPMode::CancelMaker);
assert_eq!(book.stp_mode(), STPMode::CancelMaker);
book.set_stp_mode(STPMode::CancelBoth);
assert_eq!(book.stp_mode(), STPMode::CancelBoth);
book.set_stp_mode(STPMode::None);
assert_eq!(book.stp_mode(), STPMode::None);
}
#[test]
fn test_with_stp_mode_constructor() {
let book: OrderBook<()> = OrderBook::with_stp_mode("TEST", STPMode::CancelBoth);
assert_eq!(book.stp_mode(), STPMode::CancelBoth);
assert_eq!(book.symbol(), "TEST");
}
#[test]
fn test_stp_empty_book_returns_insufficient_liquidity() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 10, Side::Buy, user(1));
match result {
Err(OrderBookError::InsufficientLiquidity { .. }) => {}
other => panic!("expected InsufficientLiquidity, got {other:?}"),
}
}
#[test]
fn test_stp_limit_order_no_cross_adds_to_book() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
add_sell_order_with_user(&book, 200, 10, same_user);
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(5),
side: Side::Buy,
user_id: same_user,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
assert_eq!(book.best_bid(), Some(100));
}
#[test]
fn test_stp_submit_market_order_with_user() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.submit_market_order_with_user(taker_id, 10, Side::Buy, same_user);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelTaker);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
}
#[test]
fn test_stp_match_limit_order_with_user() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_limit_order_with_user(taker_id, 10, Side::Buy, 100, same_user);
match result {
Err(OrderBookError::SelfTradePrevented { mode, .. }) => {
assert_eq!(mode, STPMode::CancelTaker);
}
other => panic!("expected SelfTradePrevented, got {other:?}"),
}
}
#[test]
fn test_stp_backward_compat_no_user_id() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let same_user = user(1);
add_sell_order_with_user(&book, 100, 10, same_user);
let taker_id = Id::new();
let result = book.match_market_order(taker_id, 10, Side::Buy);
assert!(result.is_ok());
let mr = result.unwrap();
assert!(mr.is_complete());
}
#[test]
fn test_stp_multiple_levels_cancel_taker() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(1);
let other_user = user(2);
add_sell_order_with_user(&book, 100, 5, other_user);
add_sell_order_with_user(&book, 200, 10, taker_user);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 20, Side::Buy, taker_user);
assert!(result.is_ok());
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), 5);
assert!(!mr.is_complete());
}
#[test]
fn test_missing_user_id_limit_order_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let result = book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_missing_user_id_limit_order_stp_disabled() {
let book: OrderBook<()> = OrderBook::new("TEST");
assert_eq!(book.stp_mode(), STPMode::None);
let result = book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_ok());
}
#[test]
fn test_limit_order_with_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let result = book.add_limit_order_with_user(
Id::new(),
100,
10,
Side::Buy,
TimeInForce::Gtc,
user(1),
None,
);
assert!(result.is_ok());
}
#[test]
fn test_limit_order_with_zero_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let result = book.add_limit_order_with_user(
Id::new(),
100,
10,
Side::Buy,
TimeInForce::Gtc,
Hash32::zero(),
None,
);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_missing_user_id_iceberg_order_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let result =
book.add_iceberg_order(Id::new(), 100, 5, 10, Side::Sell, TimeInForce::Gtc, None);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_iceberg_order_with_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let result = book.add_iceberg_order_with_user(
Id::new(),
100,
5,
10,
Side::Sell,
TimeInForce::Gtc,
user(2),
None,
);
assert!(result.is_ok());
}
#[test]
fn test_iceberg_order_stp_disabled() {
let book: OrderBook<()> = OrderBook::new("TEST");
let result =
book.add_iceberg_order(Id::new(), 100, 5, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(result.is_ok());
}
#[test]
fn test_missing_user_id_post_only_order_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let result =
book.add_post_only_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_post_only_order_with_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let result = book.add_post_only_order_with_user(
Id::new(),
200,
10,
Side::Sell,
TimeInForce::Gtc,
user(3),
None,
);
assert!(result.is_ok());
}
#[test]
fn test_post_only_order_stp_disabled() {
let book: OrderBook<()> = OrderBook::new("TEST");
let result =
book.add_post_only_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_ok());
}
#[test]
fn test_add_order_direct_zero_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_add_order_direct_nonzero_user_stp_enabled() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: user(5),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_missing_user_id_all_stp_modes() {
for mode in [
STPMode::CancelTaker,
STPMode::CancelMaker,
STPMode::CancelBoth,
] {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(mode);
let result =
book.add_limit_order(Id::new(), 100, 10, Side::Buy, TimeInForce::Gtc, None);
match result {
Err(OrderBookError::MissingUserId { .. }) => {}
other => panic!("expected MissingUserId for mode {mode}, got {other:?}"),
}
}
}
#[test]
fn test_missing_user_id_error_contains_order_id() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let oid = Id::new();
let order = OrderType::Standard {
id: oid,
price: Price::new(100),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
let result = book.add_order(order);
match result {
Err(OrderBookError::MissingUserId { order_id }) => {
assert_eq!(order_id, oid);
}
other => panic!("expected MissingUserId, got {other:?}"),
}
}
#[test]
fn test_missing_user_id_display() {
let oid = Id::new();
let err = OrderBookError::MissingUserId { order_id: oid };
let msg = err.to_string();
assert!(msg.contains("missing user_id"));
assert!(msg.contains("STP"));
}
}