#[cfg(test)]
mod tests {
use crate::{OrderBook, OrderBookError};
use pricelevel::{Id, Price, Quantity, Side, TimeInForce};
fn new_order_id() -> Id {
Id::new_uuid()
}
fn create_test_order_book() -> OrderBook<()> {
OrderBook::new("TEST-SYMBOL")
}
#[test]
fn test_add_limit_order() {
let order_book = create_test_order_book();
let id = new_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let time_in_force = TimeInForce::Gtc;
let result = order_book.add_limit_order(id, price, quantity, side, time_in_force, None);
assert!(result.is_ok(), "Adding a limit order should succeed");
let order = result.unwrap();
assert_eq!(order.id(), id, "Order ID should match");
assert_eq!(order.price().as_u128(), price, "Price should match");
assert_eq!(order.visible_quantity(), quantity, "Quantity should match");
assert_eq!(order.side(), side, "Side should match");
assert_eq!(
order.time_in_force(),
time_in_force,
"Time in force should match"
);
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_add_iceberg_order() {
let order_book = create_test_order_book();
let id = new_order_id();
let price = 1000;
let visible_quantity = 10;
let hidden_quantity = 90;
let side = Side::Sell;
let time_in_force = TimeInForce::Gtc;
let result = order_book.add_iceberg_order(
id,
price,
visible_quantity,
hidden_quantity,
side,
time_in_force,
None,
);
assert!(result.is_ok(), "Adding an iceberg order should succeed");
let order = result.unwrap();
assert_eq!(order.id(), id, "Order ID should match");
assert_eq!(order.price().as_u128(), price, "Price should match");
assert_eq!(
order.visible_quantity(),
visible_quantity,
"Visible quantity should match"
);
assert_eq!(
order.hidden_quantity(),
hidden_quantity,
"Hidden quantity should match"
);
assert_eq!(order.side(), side, "Side should match");
assert_eq!(
order.time_in_force(),
time_in_force,
"Time in force should match"
);
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_add_post_only_order() {
let order_book = create_test_order_book();
let id = new_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let time_in_force = TimeInForce::Gtc;
let result = order_book.add_post_only_order(id, price, quantity, side, time_in_force, None);
assert!(result.is_ok(), "Adding a post-only order should succeed");
let order = result.unwrap();
assert_eq!(order.id(), id, "Order ID should match");
assert_eq!(order.price().as_u128(), price, "Price should match");
assert_eq!(order.visible_quantity(), quantity, "Quantity should match");
assert_eq!(order.side(), side, "Side should match");
assert_eq!(
order.time_in_force(),
time_in_force,
"Time in force should match"
);
assert!(order.is_post_only(), "Order should be post-only");
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_post_only_order_price_crossing() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let buy_result =
order_book.add_post_only_order(buy_id, 1000, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(
buy_result.is_err(),
"Post-only order that would cross should fail"
);
match buy_result {
Err(OrderBookError::PriceCrossing {
price,
side,
opposite_price,
}) => {
assert_eq!(price, 1000, "Price should match");
assert_eq!(side, Side::Buy, "Side should be buy");
assert_eq!(opposite_price, 1000, "Opposite price should match");
}
_ => panic!("Expected PriceCrossing error"),
}
}
#[test]
fn test_submit_market_order() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let market_result = order_book.submit_market_order(
buy_id,
5, Side::Buy,
);
assert!(market_result.is_ok(), "Market order should succeed");
let match_result = market_result.unwrap();
assert_eq!(match_result.order_id(), buy_id, "Order ID should match");
assert_eq!(
match_result.executed_quantity().unwrap(),
5,
"Should execute requested quantity"
);
assert_eq!(
match_result.remaining_quantity(),
0,
"No remaining quantity"
);
assert!(match_result.is_complete(), "Order should be complete");
assert_eq!(
match_result.trades().len(),
1,
"Should have one transaction"
);
let transaction = &match_result.trades().as_vec()[0];
assert_eq!(
transaction.taker_order_id(),
buy_id,
"Taker should be market order"
);
assert_eq!(
transaction.maker_order_id(),
sell_id,
"Maker should be limit order"
);
assert_eq!(
transaction.price(),
Price::new(1000),
"Price should match limit order price"
);
assert_eq!(
transaction.quantity(),
Quantity::new(5),
"Quantity should match market order size"
);
assert_eq!(
transaction.taker_side(),
Side::Buy,
"Taker side should be buy"
);
let updated_sell = order_book.get_order(sell_id);
assert!(
updated_sell.is_some(),
"Sell order should still be in the book"
);
assert_eq!(
updated_sell.unwrap().visible_quantity(),
5,
"Sell order should have reduced quantity"
);
}
#[test]
fn test_submit_market_order_full_fill() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let market_result = order_book.submit_market_order(buy_id, 10, Side::Buy);
assert!(market_result.is_ok(), "Market order should succeed");
let match_result = market_result.unwrap();
assert_eq!(
match_result.executed_quantity().unwrap(),
10,
"Should execute full quantity"
);
assert!(
match_result.filled_order_ids().contains(&sell_id),
"Sell order should be marked as filled"
);
let updated_sell = order_book.get_order(sell_id);
assert!(
updated_sell.is_none(),
"Sell order should be removed from the book"
);
}
#[test]
fn test_submit_market_order_insufficient_liquidity() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let market_result = order_book.submit_market_order(buy_id, 20, Side::Buy);
assert!(
market_result.is_ok(),
"Market order should succeed with partial fill"
);
let match_result = market_result.unwrap();
assert_eq!(
match_result.executed_quantity().unwrap(),
10,
"Should execute available quantity"
);
assert_eq!(
match_result.remaining_quantity(),
10,
"Should have remaining quantity"
);
assert!(!match_result.is_complete(), "Order should not be complete");
}
#[test]
fn test_market_order_no_liquidity() {
let order_book = create_test_order_book();
let buy_id = new_order_id();
let market_result = order_book.submit_market_order(buy_id, 10, Side::Buy);
assert!(
market_result.is_err(),
"Market order with no liquidity should fail"
);
match market_result {
Err(OrderBookError::InsufficientLiquidity {
side,
requested,
available,
}) => {
assert_eq!(side, Side::Buy, "Side should be buy");
assert_eq!(requested, 10, "Requested quantity should match");
assert_eq!(available, 0, "Available should be zero");
}
_ => panic!("Expected InsufficientLiquidity error"),
}
}
#[test]
fn test_limit_order_immediate_or_cancel() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let buy_result = order_book.add_limit_order(
buy_id,
1000, 5, Side::Buy,
TimeInForce::Ioc,
None,
);
assert!(buy_result.is_ok(), "Adding IOC buy order should succeed");
let buy_order = order_book.get_order(buy_id);
assert!(
buy_order.is_none(),
"IOC order should not be in the book after execution"
);
let sell_order = order_book.get_order(sell_id);
assert!(
sell_order.is_some(),
"Sell order should still be in the book"
);
assert_eq!(
sell_order.unwrap().visible_quantity(),
5,
"Sell order should have reduced quantity"
);
}
#[test]
fn test_limit_order_fill_or_kill_success() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result =
order_book.add_limit_order(sell_id, 1000, 10, Side::Sell, TimeInForce::Gtc, None);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let buy_result = order_book.add_limit_order(
buy_id,
1000,
10, Side::Buy,
TimeInForce::Fok,
None,
);
assert!(
buy_result.is_ok(),
"Adding FOK buy order should succeed when fully fillable"
);
let buy_order = order_book.get_order(buy_id);
assert!(
buy_order.is_none(),
"FOK order should not be in the book after execution"
);
let sell_order = order_book.get_order(sell_id);
assert!(
sell_order.is_none(),
"Sell order should be removed from the book"
);
}
#[test]
fn test_limit_order_fill_or_kill_failure() {
let order_book = create_test_order_book();
let sell_id = new_order_id();
let sell_result = order_book.add_limit_order(
sell_id,
1000,
5, Side::Sell,
TimeInForce::Gtc,
None,
);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = new_order_id();
let buy_result = order_book.add_limit_order(
buy_id,
1000,
10, Side::Buy,
TimeInForce::Fok,
None,
);
assert!(
buy_result.is_err(),
"Adding FOK buy order should fail when not fully fillable"
);
match buy_result {
Err(OrderBookError::InsufficientLiquidity {
side,
requested,
available,
}) => {
assert_eq!(side, Side::Buy, "Side should be buy");
assert_eq!(requested, 10, "Requested quantity should match");
assert_eq!(available, 5, "Available quantity should match");
}
_ => panic!("Expected InsufficientLiquidity error"),
}
let sell_order = order_book.get_order(sell_id);
assert!(
sell_order.is_some(),
"Sell order should still be in the book"
);
assert_eq!(
sell_order.unwrap().visible_quantity(),
5,
"Sell order quantity should be unchanged"
);
}
}
#[cfg(test)]
mod test_extra_fields {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
struct OrderMetadata {
client_id: String,
strategy: String,
priority: u32,
}
fn create_order_id() -> Id {
Id::new_uuid()
}
fn create_test_metadata() -> OrderMetadata {
OrderMetadata {
client_id: "client_123".to_string(),
strategy: "momentum".to_string(),
priority: 1,
}
}
#[test]
fn test_add_limit_order_with_extra_fields() {
let order_book: OrderBook<OrderMetadata> = OrderBook::new("TEST-SYMBOL");
let id = create_order_id();
let metadata = create_test_metadata();
let result = order_book.add_limit_order(
id,
1000,
10,
Side::Buy,
TimeInForce::Gtc,
Some(metadata.clone()),
);
assert!(
result.is_ok(),
"Adding limit order with extra fields should succeed"
);
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), 1000);
assert_eq!(order.visible_quantity(), 10);
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_add_iceberg_order_with_extra_fields() {
let order_book: OrderBook<OrderMetadata> = OrderBook::new("TEST-SYMBOL");
let id = create_order_id();
let metadata = OrderMetadata {
client_id: "client_456".to_string(),
strategy: "iceberg_algo".to_string(),
priority: 2,
};
let result = order_book.add_iceberg_order(
id,
1000,
10,
90,
Side::Sell,
TimeInForce::Gtc,
Some(metadata),
);
assert!(
result.is_ok(),
"Adding iceberg order with extra fields should succeed"
);
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), 1000);
assert_eq!(order.visible_quantity(), 10);
assert_eq!(order.hidden_quantity(), 90);
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_add_post_only_order_with_extra_fields() {
let order_book: OrderBook<OrderMetadata> = OrderBook::new("TEST-SYMBOL");
let id = create_order_id();
let metadata = OrderMetadata {
client_id: "client_789".to_string(),
strategy: "post_only_maker".to_string(),
priority: 3,
};
let result = order_book.add_post_only_order(
id,
1000,
10,
Side::Buy,
TimeInForce::Gtc,
Some(metadata),
);
assert!(
result.is_ok(),
"Adding post-only order with extra fields should succeed"
);
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), 1000);
assert_eq!(order.visible_quantity(), 10);
assert!(order.is_post_only());
let book_order = order_book.get_order(id);
assert!(book_order.is_some(), "Order should be in the book");
}
#[test]
fn test_mixed_orders_with_and_without_extra_fields() {
let order_book: OrderBook<OrderMetadata> = OrderBook::new("TEST-SYMBOL");
let id1 = create_order_id();
let result1 = order_book.add_limit_order(id1, 1000, 10, Side::Buy, TimeInForce::Gtc, None);
assert!(result1.is_ok(), "Order without extra fields should succeed");
let id2 = create_order_id();
let metadata = create_test_metadata();
let result2 =
order_book.add_limit_order(id2, 1001, 15, Side::Buy, TimeInForce::Gtc, Some(metadata));
assert!(result2.is_ok(), "Order with extra fields should succeed");
assert!(order_book.get_order(id1).is_some());
assert!(order_book.get_order(id2).is_some());
}
#[test]
fn test_market_order_matching_with_extra_fields() {
let order_book: OrderBook<OrderMetadata> = OrderBook::new("TEST-SYMBOL");
let sell_id = create_order_id();
let sell_metadata = OrderMetadata {
client_id: "seller_123".to_string(),
strategy: "market_making".to_string(),
priority: 1,
};
let sell_result = order_book.add_limit_order(
sell_id,
1000,
10,
Side::Sell,
TimeInForce::Gtc,
Some(sell_metadata),
);
assert!(sell_result.is_ok(), "Adding sell order should succeed");
let buy_id = create_order_id();
let market_result = order_book.submit_market_order(buy_id, 5, Side::Buy);
assert!(market_result.is_ok(), "Market order should succeed");
let match_result = market_result.unwrap();
assert_eq!(match_result.executed_quantity().unwrap(), 5);
assert_eq!(match_result.trades().len(), 1);
let remaining_sell = order_book.get_order(sell_id);
assert!(remaining_sell.is_some());
assert_eq!(remaining_sell.unwrap().visible_quantity(), 5);
}
}
#[cfg(test)]
mod test_operations_remaining {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_add_limit_order_with_trace() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let time_in_force = TimeInForce::Gtc;
let result = book.add_limit_order(id, price, quantity, side, time_in_force, None);
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), price);
assert_eq!(order.visible_quantity(), quantity);
}
#[test]
fn test_add_iceberg_order_with_trace() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let visible_quantity = 10;
let hidden_quantity = 90;
let side = Side::Sell;
let time_in_force = TimeInForce::Gtc;
let result = book.add_iceberg_order(
id,
price,
visible_quantity,
hidden_quantity,
side,
time_in_force,
None,
);
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), price);
assert_eq!(order.visible_quantity(), visible_quantity);
assert_eq!(order.hidden_quantity(), hidden_quantity);
}
#[test]
fn test_add_post_only_order_with_trace() {
let book: OrderBook<()> = OrderBook::new("TEST");
let id = create_order_id();
let price = 1000;
let quantity = 10;
let side = Side::Buy;
let time_in_force = TimeInForce::Gtc;
let result = book.add_post_only_order(id, price, quantity, side, time_in_force, None);
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.id(), id);
assert_eq!(order.price().as_u128(), price);
assert_eq!(order.visible_quantity(), quantity);
assert!(order.is_post_only());
}
}
#[cfg(test)]
mod test_operations_specific {
use crate::OrderBook;
use pricelevel::{Id, Side, TimeInForce};
use tracing::trace;
fn create_order_id() -> Id {
Id::new_uuid()
}
#[test]
fn test_submit_market_order_with_tracing() {
let book: OrderBook<()> = OrderBook::new("TEST");
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();
trace!(
"About to submit market order {} for {} at side {:?}",
buy_id,
5,
Side::Buy
);
let result = book.submit_market_order(buy_id, 5, Side::Buy);
assert!(result.is_ok());
let remaining_sell = book.get_order(sell_id);
assert!(remaining_sell.is_some());
assert_eq!(remaining_sell.unwrap().visible_quantity(), 5);
}
}