#[cfg(test)]
mod tests {
use crate::orderbook::book::OrderBook;
use crate::orderbook::error::OrderBookError;
use crate::orderbook::stp::STPMode;
use pricelevel::{
Hash32, Id, OrderType, OrderUpdate, 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
}
fn add_sell_order_with_ts(
book: &OrderBook<()>,
price: u128,
quantity: u64,
user_id: Hash32,
timestamp: u64,
) -> Id {
let order = OrderType::Standard {
id: Id::new(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side: Side::Sell,
user_id,
timestamp: TimestampMs::new(timestamp),
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
}
#[test]
fn test_cancel_taker_scan_follows_time_priority_at_one_level() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(9);
let other = user(1);
let m1 = add_sell_order_with_ts(&book, 100, 3, other, 10);
let m2 = add_sell_order_with_ts(&book, 100, 4, other, 20);
let m_self = add_sell_order_with_ts(&book, 100, 5, taker_user, 30);
let m3 = add_sell_order_with_ts(&book, 100, 6, other, 40);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 50, Side::Buy, taker_user);
assert!(result.is_ok(), "expected partial fill, got {result:?}");
let mr = result.unwrap();
assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(7));
let trades = mr.trades().as_vec();
assert_eq!(
trades.len(),
2,
"should fill exactly the two older non-self orders"
);
assert_eq!(trades[0].maker_order_id(), m1);
assert_eq!(trades[0].quantity(), Quantity::new(3));
assert_eq!(trades[1].maker_order_id(), m2);
assert_eq!(trades[1].quantity(), Quantity::new(4));
assert!(
book.get_order(m_self).is_some(),
"self maker must survive STP"
);
assert!(
book.get_order(m3).is_some(),
"later order must not be reached"
);
assert!(book.get_order(m1).is_none());
assert!(book.get_order(m2).is_none());
}
#[test]
fn test_cancel_both_cancels_earliest_self_maker_by_time() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let taker_user = user(9);
let other = user(1);
let o1 = add_sell_order_with_ts(&book, 100, 8, other, 10);
let self1 = add_sell_order_with_ts(&book, 100, 5, taker_user, 20);
let self2 = add_sell_order_with_ts(&book, 100, 6, taker_user, 30);
let taker_id = Id::new();
let result = book.match_market_order_with_user(taker_id, 50, Side::Buy, taker_user);
assert!(result.is_ok(), "expected partial fill, got {result:?}");
let mr = result.unwrap();
let trades = mr.trades().as_vec();
assert_eq!(trades.len(), 1);
assert_eq!(trades[0].maker_order_id(), o1);
assert_eq!(trades[0].quantity(), Quantity::new(8));
assert!(
book.get_order(self1).is_none(),
"earliest self maker should be cancelled"
);
assert!(
book.get_order(self2).is_some(),
"later self maker should remain"
);
assert!(
book.get_order(o1).is_none(),
"older non-self order fully consumed"
);
}
#[test]
fn test_cancel_taker_scan_follows_insertion_not_timestamp_order() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(9);
let other = user(1);
let m_other = add_sell_order_with_ts(&book, 100, 4, other, 30);
let m_self = add_sell_order_with_ts(&book, 100, 5, taker_user, 10);
let result = book.match_market_order_with_user(Id::new(), 20, Side::Buy, taker_user);
assert!(
result.is_ok(),
"expected a 4-unit partial fill, got {result:?}"
);
let mr = result.unwrap();
let trades = mr.trades().as_vec();
assert_eq!(trades.len(), 1);
assert_eq!(trades[0].maker_order_id(), m_other);
assert_eq!(trades[0].quantity(), Quantity::new(4));
assert!(
book.get_order(m_self).is_some(),
"self order survives CancelTaker"
);
assert!(
book.get_order(m_other).is_none(),
"other-user order is consumed first by insertion order"
);
}
#[test]
fn test_stp_cancel_maker_emits_event_and_records_state() {
use crate::orderbook::book_change_event::PriceLevelChangedEvent;
use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
use std::sync::{Arc, Mutex};
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
book.set_order_state_tracker(OrderStateTracker::new());
let u = user(7);
let maker = add_sell_order_with_user(&book, 100, 10, u);
let events: Arc<Mutex<Vec<PriceLevelChangedEvent>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&events);
book.set_price_level_listener(Arc::new(move |e: PriceLevelChangedEvent| {
sink.lock().expect("event sink").push(e);
}));
let taker = Id::new();
let _ = book.match_market_order_with_user(taker, 10, Side::Buy, u);
match book.order_status(maker) {
Some(OrderStatus::Cancelled { reason, .. }) => {
assert_eq!(reason, CancelReason::SelfTradePrevention);
}
other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
}
let captured = events.lock().expect("event sink");
assert!(
captured
.iter()
.any(|e| e.price == 100 && e.side == Side::Sell),
"expected a Sell-side PriceLevelChangedEvent at price 100 for the cancelled maker's level"
);
}
#[test]
fn test_cancel_maker_same_other_same_cancels_both_self_fills_other_issue_107() {
use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
book.set_order_state_tracker(OrderStateTracker::new());
let taker_user = user(9);
let other = user(1);
let self_a = add_sell_order_with_user(&book, 100, 10, taker_user);
let other_b = add_sell_order_with_user(&book, 100, 7, other);
let self_c = add_sell_order_with_user(&book, 100, 20, taker_user);
let taker = Id::new();
let result = book.match_market_order_with_user(taker, 100, Side::Buy, taker_user);
assert!(
result.is_ok(),
"expected a fill against the non-self maker, got {result:?}"
);
let mr = result.unwrap();
let trades = mr.trades().as_vec();
assert_eq!(trades.len(), 1, "only the non-self maker fills");
assert_eq!(trades[0].maker_order_id(), other_b);
assert_eq!(trades[0].quantity(), Quantity::new(7));
assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(7));
for self_id in [self_a, self_c] {
assert!(
book.get_order(self_id).is_none(),
"same-user maker must be STP-cancelled"
);
match book.order_status(self_id) {
Some(OrderStatus::Cancelled { reason, .. }) => {
assert_eq!(reason, CancelReason::SelfTradePrevention);
}
other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
}
}
assert!(
book.get_order(other_b).is_none(),
"non-self maker fully filled"
);
}
#[test]
fn test_stp_cancel_both_frees_risk_slot_and_records_state() {
use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
use crate::orderbook::risk::RiskConfig;
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
book.set_risk_config(RiskConfig::new().with_max_open_orders_per_account(1));
book.set_order_state_tracker(OrderStateTracker::new());
let u = user(7);
let maker = Id::new();
book.add_limit_order_with_user(maker, 100, 10, Side::Sell, TimeInForce::Gtc, u, None)
.expect("maker admitted (1/1)");
let taker = Id::new();
let _ = book.match_market_order_with_user(taker, 5, Side::Buy, u);
match book.order_status(maker) {
Some(OrderStatus::Cancelled { reason, .. }) => {
assert_eq!(reason, CancelReason::SelfTradePrevention);
}
other => panic!("expected Cancelled {{ SelfTradePrevention }}, got {other:?}"),
}
book.add_limit_order_with_user(Id::new(), 101, 1, Side::Sell, TimeInForce::Gtc, u, None)
.expect("STP cancel must free the per-account risk slot");
}
#[test]
fn test_fok_under_stp_cancel_maker_kills_without_touching_makers() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let u = user(7);
let maker = add_sell_order_with_user(&book, 100, 10, u);
let fok = OrderType::Standard {
id: Id::new(),
price: Price::new(100),
quantity: Quantity::new(10),
side: Side::Buy,
user_id: u,
time_in_force: TimeInForce::Fok,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
extra_fields: (),
};
let result = book.add_order(fok);
assert!(
matches!(result, Err(OrderBookError::InsufficientLiquidity { .. })),
"FOK must be killed when only self-liquidity exists, got {result:?}"
);
assert!(
book.get_order(maker).is_some(),
"FOK kill must not cancel the resting maker"
);
assert!(
!book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
"FOK kill must emit no trades"
);
}
#[test]
fn test_cancel_taker_partial_self_cross_does_not_rest_residual() {
use crate::orderbook::order_state::{CancelReason, OrderStateTracker, OrderStatus};
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
book.set_order_state_tracker(OrderStateTracker::new());
let taker_user = user(7);
let other = user(1);
let other_maker = add_sell_order_with_user(&book, 100, 5, other);
let self_maker = add_sell_order_with_user(&book, 200, 10, taker_user);
let taker_id = Id::new();
let taker = OrderType::Standard {
id: taker_id,
price: Price::new(200),
quantity: Quantity::new(20),
side: Side::Buy,
user_id: taker_user,
time_in_force: TimeInForce::Gtc,
timestamp: TimestampMs::new(crate::utils::current_time_millis()),
extra_fields: (),
};
let result = book.add_order(taker);
assert!(
matches!(result, Err(OrderBookError::SelfTradePrevented { .. })),
"partial self-cross under CancelTaker must surface the STP error, got {result:?}"
);
assert!(
book.get_order(taker_id).is_none(),
"STP-cancelled taker must not rest its residual"
);
assert!(book.bids.is_empty(), "no residual bid level should exist");
match book.order_status(taker_id) {
Some(OrderStatus::Cancelled {
filled_quantity,
reason,
}) => {
assert_eq!(reason, CancelReason::SelfTradePrevention);
assert_eq!(
filled_quantity, 5,
"filled quantity must reflect the 5-unit non-self fill"
);
}
other => panic!("expected Cancelled {{ SelfTradePrevention, 5 }}, got {other:?}"),
}
assert!(book.get_order(other_maker).is_none());
assert!(book.get_order(self_maker).is_some());
}
#[test]
fn test_notional_market_partial_self_cross_returns_partial() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let taker_user = user(7);
let other = user(1);
let other_maker = add_sell_order_with_user(&book, 100, 5, other);
let self_maker = add_sell_order_with_user(&book, 200, 10, taker_user);
let taker_id = Id::new();
let result =
book.match_market_order_by_amount_with_user(taker_id, 1000, Side::Buy, taker_user);
let mr = result.expect("notional partial self-cross returns Ok(partial)");
assert_eq!(mr.executed_quantity().unwrap(), Quantity::new(5));
assert!(book.get_order(taker_id).is_none(), "no residual rests");
assert!(book.bids.is_empty());
assert!(book.get_order(other_maker).is_none());
assert!(book.get_order(self_maker).is_some());
}
#[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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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(), Quantity::new(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"));
}
#[test]
fn test_modify_self_cross_under_cancel_taker_rejected_preserves_original_issue_168() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let u = user(7);
let buy_id = add_buy_order_with_user(&book, 100, 10, u);
let sell_id = add_sell_order_with_user(&book, 110, 10, u);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(110),
});
assert!(
matches!(
res,
Err(OrderBookError::SelfTradePrevented { taker_order_id, .. }) if taker_order_id == buy_id
),
"self-crossing modify must be STP-rejected pre-cancel, got {res:?}"
);
let buy = book.get_order(buy_id).expect("original buy must survive");
assert_eq!(
buy.price().as_u128(),
100,
"original price must be unchanged"
);
assert!(
book.get_order(sell_id).is_some(),
"the maker must be untouched"
);
assert!(
!book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
"a rejected modify must emit no trades"
);
}
#[test]
fn test_modify_self_cross_under_cancel_both_rejected_preserves_original_issue_168() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelBoth);
let u = user(8);
let sell_id = add_sell_order_with_user(&book, 110, 10, u);
let bid_maker = add_buy_order_with_user(&book, 90, 10, u);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: sell_id,
new_price: Price::new(90),
});
assert!(
matches!(res, Err(OrderBookError::SelfTradePrevented { .. })),
"self-crossing modify under CancelBoth must be rejected pre-cancel, got {res:?}"
);
let sell = book.get_order(sell_id).expect("original sell must survive");
assert_eq!(sell.price().as_u128(), 110, "original price unchanged");
assert!(
book.get_order(bid_maker).is_some(),
"the bid maker untouched"
);
}
#[test]
fn test_modify_self_cross_under_cancel_maker_not_prerejected_issue_168() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelMaker);
let u = user(9);
let buy_id = add_buy_order_with_user(&book, 100, 10, u);
let sell_id = add_sell_order_with_user(&book, 110, 10, u);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(110),
});
assert!(
res.is_ok(),
"CancelMaker modify must not be pre-rejected, got {res:?}"
);
assert!(
book.get_order(buy_id).is_some(),
"the re-priced taker rests"
);
assert!(
book.get_order(sell_id).is_none(),
"the same-user maker is cancelled by CancelMaker"
);
}
#[test]
fn test_modify_cross_other_user_under_cancel_taker_allowed_issue_168() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let u = user(1);
let other = user(2);
let buy_id = add_buy_order_with_user(&book, 100, 10, u);
let _other_sell = add_sell_order_with_user(&book, 110, 10, other);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(110),
});
assert!(
res.is_ok(),
"crossing another user's order is not a self-cross, must succeed, got {res:?}"
);
assert!(
book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
"the re-priced buy fills against the other user's sell"
);
}
#[test]
fn test_modify_self_cross_taker_fills_before_self_maker_allowed_issue_168() {
let mut book: OrderBook<()> = OrderBook::new("TEST");
book.set_stp_mode(STPMode::CancelTaker);
let u = user(3);
let other = user(4);
let buy_id = add_buy_order_with_user(&book, 100, 5, u);
let _other_sell = add_sell_order_with_user(&book, 105, 10, other);
let self_sell = add_sell_order_with_user(&book, 110, 10, u);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(110),
});
assert!(
res.is_ok(),
"taker satisfied before the same-user maker must not be rejected, got {res:?}"
);
assert!(book.has_traded.load(std::sync::atomic::Ordering::SeqCst));
assert!(
book.get_order(self_sell).is_some(),
"the same-user maker beyond the fill point is untouched"
);
}
#[test]
fn test_modify_self_cross_under_cancel_taker_with_lot_size_rejected_issue_168() {
let mut book: OrderBook<()> = OrderBook::with_lot_size("TEST", 5);
book.set_stp_mode(STPMode::CancelTaker);
let u = user(5);
let other = user(6);
let buy_id = add_buy_order_with_user(&book, 100, 10, u);
let _other_sell = add_sell_order_with_user(&book, 105, 5, other);
let self_sell = add_sell_order_with_user(&book, 110, 10, u);
let res = book.update_order(OrderUpdate::UpdatePrice {
order_id: buy_id,
new_price: Price::new(110),
});
assert!(
matches!(res, Err(OrderBookError::SelfTradePrevented { .. })),
"lot-sized self-cross must be rejected pre-cancel, got {res:?}"
);
let buy = book.get_order(buy_id).expect("original buy survives");
assert_eq!(buy.price().as_u128(), 100, "original price unchanged");
assert!(
book.get_order(self_sell).is_some(),
"same-user maker untouched"
);
assert!(
!book.has_traded.load(std::sync::atomic::Ordering::SeqCst),
"a rejected modify emits no trades"
);
}
}