use orderbook_rs::{OrderBook, OrderBookError};
use pricelevel::{Hash32, Id, OrderType, Price, Quantity, Side, TimeInForce, TimestampMs};
#[cfg(test)]
mod tests {
use super::*;
fn make_standard_order(price: u128, quantity: u64, side: Side) -> OrderType<()> {
OrderType::Standard {
id: Id::new_uuid(),
price: Price::new(price),
quantity: Quantity::new(quantity),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
fn make_iceberg_order(price: u128, side: Side) -> OrderType<()> {
make_iceberg_order_with_qty(price, 10, 90, side)
}
fn make_iceberg_order_with_qty(
price: u128,
visible: u64,
hidden: u64,
side: Side,
) -> OrderType<()> {
OrderType::IcebergOrder {
id: Id::new_uuid(),
price: Price::new(price),
visible_quantity: Quantity::new(visible),
hidden_quantity: Quantity::new(hidden),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
fn make_post_only_order(price: u128, side: Side) -> OrderType<()> {
OrderType::PostOnly {
id: Id::new_uuid(),
price: Price::new(price),
quantity: Quantity::new(100),
side,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
}
}
#[test]
fn test_with_tick_size_constructor() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
assert_eq!(book.tick_size(), Some(100));
}
#[test]
fn test_set_tick_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
assert_eq!(book.tick_size(), None);
book.set_tick_size(50);
assert_eq!(book.tick_size(), Some(50));
}
#[test]
fn test_no_tick_size_accepts_any_price() {
let book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(12345, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_valid_tick_size_exact_multiple() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(1000, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_valid_tick_size_larger_multiple() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(50000, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_valid_tick_size_equals_tick() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(100, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_invalid_tick_size_not_multiple() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(150, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let err = result.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("tick size"),
"Error message should mention tick size: {msg}"
);
}
#[test]
fn test_invalid_tick_size_off_by_one() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(1001, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
}
#[test]
fn test_invalid_tick_size_off_by_one_below() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(999, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
}
#[test]
fn test_tick_size_one_accepts_any_price() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 1);
let order = make_standard_order(12345, 100, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_tick_size_rejects_iceberg_order() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_iceberg_order(150, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
}
#[test]
fn test_tick_size_accepts_iceberg_order_valid() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_iceberg_order(200, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_tick_size_rejects_post_only_order() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_post_only_order(150, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
}
#[test]
fn test_tick_size_accepts_post_only_order_valid() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_post_only_order(200, Side::Buy);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_tick_size_rejects_sell_order() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(150, 100, Side::Sell);
let result = book.add_order(order);
assert!(result.is_err());
}
#[test]
fn test_tick_size_accepts_sell_order_valid() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(200, 100, Side::Sell);
let result = book.add_order(order);
assert!(result.is_ok());
}
#[test]
fn test_set_tick_size_changes_validation() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(150, 100, Side::Buy);
assert!(book.add_order(order).is_ok());
book.set_tick_size(100);
let order = make_standard_order(150, 100, Side::Sell);
assert!(book.add_order(order).is_err());
let order = make_standard_order(200, 100, Side::Sell);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_add_limit_order_respects_tick_size() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let result =
book.add_limit_order(Id::new_uuid(), 150, 100, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_err());
}
#[test]
fn test_add_limit_order_valid_tick_size() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let result =
book.add_limit_order(Id::new_uuid(), 200, 100, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_ok());
}
#[test]
fn test_invalid_tick_size_error_display() {
let book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
let order = make_standard_order(150, 100, Side::Buy);
let err = book.add_order(order).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("150"), "Should contain price: {msg}");
assert!(msg.contains("100"), "Should contain tick size: {msg}");
}
#[test]
fn test_with_lot_size_constructor() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
assert_eq!(book.lot_size(), Some(10));
}
#[test]
fn test_set_lot_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
assert_eq!(book.lot_size(), None);
book.set_lot_size(25);
assert_eq!(book.lot_size(), Some(25));
}
#[test]
fn test_no_lot_size_accepts_any_quantity() {
let book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(1000, 7, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_valid_lot_size_exact_multiple() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 100, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_valid_lot_size_equals_lot() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 10, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_invalid_lot_size_not_multiple() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 15, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("lot size"),
"Error message should mention lot size: {msg}"
);
}
#[test]
fn test_invalid_lot_size_off_by_one() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 11, Side::Buy);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_lot_size_one_accepts_any_quantity() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 1);
let order = make_standard_order(1000, 7, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_lot_size_iceberg_both_valid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_iceberg_order_with_qty(1000, 20, 80, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_lot_size_iceberg_visible_invalid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_iceberg_order_with_qty(1000, 15, 80, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("15"),
"Should contain invalid visible qty: {msg}"
);
}
#[test]
fn test_lot_size_iceberg_hidden_invalid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_iceberg_order_with_qty(1000, 20, 75, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("75"),
"Should contain invalid hidden qty: {msg}"
);
}
#[test]
fn test_lot_size_iceberg_both_invalid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_iceberg_order_with_qty(1000, 15, 75, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("15"), "Should report visible qty first: {msg}");
}
#[test]
fn test_lot_size_rejects_post_only_order() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = OrderType::PostOnly {
id: Id::new_uuid(),
price: Price::new(1000),
quantity: Quantity::new(15),
side: Side::Buy,
user_id: Hash32::zero(),
timestamp: TimestampMs::new(0),
time_in_force: TimeInForce::Gtc,
extra_fields: (),
};
assert!(book.add_order(order).is_err());
}
#[test]
fn test_lot_size_accepts_post_only_order_valid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_post_only_order(1000, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_lot_size_rejects_sell_order() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 15, Side::Sell);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_lot_size_accepts_sell_order_valid() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 50, Side::Sell);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_set_lot_size_changes_validation() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(1000, 7, Side::Buy);
assert!(book.add_order(order).is_ok());
book.set_lot_size(10);
let order = make_standard_order(2000, 7, Side::Sell);
assert!(book.add_order(order).is_err());
let order = make_standard_order(2000, 20, Side::Sell);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_add_limit_order_respects_lot_size() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let result =
book.add_limit_order(Id::new_uuid(), 1000, 15, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_err());
}
#[test]
fn test_add_limit_order_valid_lot_size() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let result =
book.add_limit_order(Id::new_uuid(), 1000, 20, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_ok());
}
#[test]
fn test_invalid_lot_size_error_display() {
let book: OrderBook<()> = OrderBook::with_lot_size("BTC/USD", 10);
let order = make_standard_order(1000, 15, Side::Buy);
let err = book.add_order(order).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("15"), "Should contain quantity: {msg}");
assert!(msg.contains("10"), "Should contain lot size: {msg}");
}
#[test]
fn test_tick_and_lot_size_both_valid() {
let mut book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
book.set_lot_size(10);
let order = make_standard_order(1000, 50, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_tick_valid_lot_invalid() {
let mut book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
book.set_lot_size(10);
let order = make_standard_order(1000, 15, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("lot size"), "Should fail on lot size: {msg}");
}
#[test]
fn test_tick_invalid_lot_valid() {
let mut book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
book.set_lot_size(10);
let order = make_standard_order(150, 50, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("tick size"),
"Should fail on tick size first: {msg}"
);
}
#[test]
fn test_set_min_order_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
assert_eq!(book.min_order_size(), None);
book.set_min_order_size(10);
assert_eq!(book.min_order_size(), Some(10));
}
#[test]
fn test_set_max_order_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
assert_eq!(book.max_order_size(), None);
book.set_max_order_size(1000);
assert_eq!(book.max_order_size(), Some(1000));
}
#[test]
fn test_no_min_max_accepts_any_quantity() {
let book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(1000, 1, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_min_order_size_rejects_below() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let order = make_standard_order(1000, 5, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("out of range"),
"Error should mention out of range: {msg}"
);
}
#[test]
fn test_min_order_size_accepts_equal() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let order = make_standard_order(1000, 10, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_min_order_size_accepts_above() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let order = make_standard_order(1000, 50, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_max_order_size_rejects_above() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(100);
let order = make_standard_order(1000, 150, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("out of range"),
"Error should mention out of range: {msg}"
);
}
#[test]
fn test_max_order_size_accepts_equal() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(100);
let order = make_standard_order(1000, 100, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_max_order_size_accepts_below() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(100);
let order = make_standard_order(1000, 50, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_only_min_set_large_quantity_accepted() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let order = make_standard_order(1000, 999_999, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_only_max_set_small_quantity_accepted() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(1000);
let order = make_standard_order(1000, 1, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_min_and_max_within_range() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
book.set_max_order_size(100);
let order = make_standard_order(1000, 50, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_min_and_max_below_min() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
book.set_max_order_size(100);
let order = make_standard_order(1000, 5, Side::Buy);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_min_and_max_above_max() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
book.set_max_order_size(100);
let order = make_standard_order(1000, 150, Side::Buy);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_max_order_size_iceberg_total_above_max() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(50);
let order = make_iceberg_order(1000, Side::Buy);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_min_order_size_iceberg_total_accepted() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(50);
let order = make_iceberg_order(1000, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_min_order_size_rejects_post_only() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(200);
let order = make_post_only_order(1000, Side::Buy);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_min_order_size_rejects_sell_below() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let order = make_standard_order(1000, 5, Side::Sell);
assert!(book.add_order(order).is_err());
}
#[test]
fn test_set_min_max_changes_validation() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
let order = make_standard_order(1000, 1, Side::Buy);
assert!(book.add_order(order).is_ok());
book.set_min_order_size(10);
let order = make_standard_order(2000, 1, Side::Sell);
assert!(book.add_order(order).is_err());
book.set_max_order_size(100);
let order = make_standard_order(3000, 200, Side::Buy);
assert!(book.add_order(order).is_err());
let order = make_standard_order(3000, 50, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_add_limit_order_respects_min_order_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
let result =
book.add_limit_order(Id::new_uuid(), 1000, 5, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_err());
}
#[test]
fn test_add_limit_order_respects_max_order_size() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_max_order_size(100);
let result =
book.add_limit_order(Id::new_uuid(), 1000, 150, Side::Buy, TimeInForce::Gtc, None);
assert!(result.is_err());
}
#[test]
fn test_order_size_out_of_range_error_display() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_min_order_size(10);
book.set_max_order_size(100);
let order = make_standard_order(1000, 5, Side::Buy);
let err = book.add_order(order).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("5"), "Should contain quantity: {msg}");
assert!(msg.contains("10"), "Should contain min: {msg}");
assert!(msg.contains("100"), "Should contain max: {msg}");
}
#[test]
fn test_all_validations_pass() {
let mut book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
book.set_lot_size(10);
book.set_min_order_size(10);
book.set_max_order_size(1000);
let order = make_standard_order(1000, 100, Side::Buy);
assert!(book.add_order(order).is_ok());
}
#[test]
fn test_tick_fails_before_min_max() {
let mut book: OrderBook<()> = OrderBook::with_tick_size("BTC/USD", 100);
book.set_min_order_size(10);
let order = make_standard_order(150, 50, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("tick size"), "Should fail on tick: {msg}");
}
#[test]
fn test_lot_fails_before_min_max() {
let mut book: OrderBook<()> = OrderBook::new("BTC/USD");
book.set_lot_size(10);
book.set_min_order_size(5);
let order = make_standard_order(1000, 7, Side::Buy);
let result = book.add_order(order);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("lot size"), "Should fail on lot: {msg}");
}
#[test]
fn test_add_order_rejects_duplicate_order_id_issue_119() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = Id::new_uuid();
let first = book.add_limit_order(id, 100, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(first.is_ok());
let dup = book.add_limit_order(id, 105, 5, Side::Sell, TimeInForce::Gtc, None);
assert!(
matches!(&dup, Err(OrderBookError::DuplicateOrderId { order_id }) if *order_id == id),
"duplicate id must be rejected: {dup:?}"
);
let cancelled = book
.cancel_order(id)
.expect("cancel succeeds")
.expect("original order still resting");
assert_eq!(
cancelled.price().as_u128(),
100,
"the rejected duplicate must not have relocated the original order"
);
let reused = book.add_limit_order(id, 110, 7, Side::Sell, TimeInForce::Gtc, None);
assert!(
reused.is_ok(),
"id is reusable once the original is removed"
);
}
}