//! Trading engine unit tests //! //! Tests order processing, matching engine, and trade execution logic //! with focus on correctness and edge case handling. // CANONICAL TYPE IMPORTS - Use types::prelude::Decimal use std::collections::HashMap; #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_order_book_creation() { let mut order_book = OrderBook::new(Symbol::new("AAPL".to_string()).unwrap()); assert_eq!(order_book.symbol().as_str(), "AAPL"); assert_eq!(order_book.best_bid(), None); assert_eq!(order_book.best_ask(), None); assert_eq!(order_book.total_bid_volume(), Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap()); assert_eq!(order_book.total_ask_volume(), Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap()); } #[tokio::test] async fn test_order_book_bid_insertion() { let mut order_book = OrderBook::new(Symbol::new("AAPL".to_string()).unwrap()); let bid_order = Order { id: OrderId::from("BID-001"), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; order_book.add_order(bid_order).expect("Should add bid order"); assert_eq!(order_book.best_bid().unwrap().value(), Decimal::from(150)); assert_eq!(order_book.total_bid_volume().value(), Decimal::from(100)); assert_eq!(order_book.best_ask(), None); } #[tokio::test] async fn test_order_book_ask_insertion() { let mut order_book = OrderBook::new(Symbol::new("AAPL".to_string()).unwrap()); let ask_order = Order { id: OrderId::from("ASK-001"), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Sell, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(200)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(155)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-002".to_string()), }; order_book.add_order(ask_order).expect("Should add ask order"); assert_eq!(order_book.best_ask().unwrap().value(), Decimal::from(155)); assert_eq!(order_book.total_ask_volume().value(), Decimal::from(200)); assert_eq!(order_book.best_bid(), None); } #[tokio::test] async fn test_order_matching_full_fill() { let mut matching_engine = MatchingEngine::new(); // Add a bid order let bid_order = Order { id: OrderId::new("BID-001".to_string()), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; let bid_result = matching_engine.submit_order(bid_order).await .expect("Should submit bid order"); assert_eq!(bid_result.status, OrderStatus::Pending); // Add a matching ask order let ask_order = Order { id: OrderId::new("ASK-001".to_string()), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Sell, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-002".to_string()), }; let ask_result = matching_engine.submit_order(ask_order).await .expect("Should submit ask order and match"); assert_eq!(ask_result.status, OrderStatus::Filled); assert_eq!(ask_result.fills.len(), 1); let fill = &ask_result.fills[0]; assert_eq!(fill.quantity.value(), Decimal::from(100)); assert_eq!(fill.price.value(), Decimal::from(150)); } #[tokio::test] async fn test_order_matching_partial_fill() { let mut matching_engine = MatchingEngine::new(); // Add a large bid order let bid_order = Order { id: OrderId::new("BID-001".to_string()), symbol: Symbol::new("MSFT".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(500)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(300)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; matching_engine.submit_order(bid_order).await.expect("Should submit bid"); // Add a smaller ask order let ask_order = Order { id: OrderId::new("ASK-001".to_string()), symbol: Symbol::new("MSFT".to_string()).unwrap(), side: Side::Sell, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(200)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(300)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-002".to_string()), }; let ask_result = matching_engine.submit_order(ask_order).await .expect("Should partial fill"); assert_eq!(ask_result.status, OrderStatus::Filled); // Check that bid order is partially filled let bid_status = matching_engine.get_order_status(&OrderId::from("BID-001")).await .expect("Should get bid status"); assert_eq!(bid_status.status, OrderStatus::PartiallyFilled); assert_eq!(bid_status.filled_quantity.value(), Decimal::from(200)); assert_eq!(bid_status.remaining_quantity.value(), Decimal::from(300)); } #[tokio::test] async fn test_market_order_execution() { let mut matching_engine = MatchingEngine::new(); // Add limit orders to create liquidity let limit_ask = Order { id: OrderId::from("ASK-LIMIT"), symbol: Symbol::new("GOOGL".to_string()).unwrap(), side: Side::Sell, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(50)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(2500)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-MM".to_string()), }; matching_engine.submit_order(limit_ask).await.expect("Should submit limit ask"); // Submit market buy order let market_order = Order { id: OrderId::from("MARKET-BUY"), symbol: Symbol::new("GOOGL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Market, quantity: Quantity::new(Decimal::from(50)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: None, // Market orders don't have price time_in_force: TimeInForce::ImmediateOrCancel, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; let result = matching_engine.submit_order(market_order).await .expect("Should execute market order"); assert_eq!(result.status, OrderStatus::Filled); assert_eq!(result.fills.len(), 1); assert_eq!(result.fills[0].price.value(), Decimal::from(2500)); // Filled at limit price } #[tokio::test] async fn test_stop_loss_order() { let mut matching_engine = MatchingEngine::new(); // Submit stop-loss order let stop_loss = Order { id: OrderId::from("STOP-001"), symbol: Symbol::new("TSLA".to_string()).unwrap(), side: Side::Sell, order_type: OrderType::StopLoss, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(800)).unwrap()), // Stop price time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; let result = matching_engine.submit_order(stop_loss).await .expect("Should submit stop-loss"); assert_eq!(result.status, OrderStatus::Pending); // Simulate price drop to trigger stop-loss matching_engine.update_market_price( &Symbol::new("TSLA".to_string()).unwrap(), Price::new(Decimal::from(795)).unwrap() ).await.expect("Should update price"); // Check if stop-loss was triggered let final_status = matching_engine.get_order_status(&OrderId::from("STOP-001")).await .expect("Should get final status"); // Stop-loss should have converted to market order and executed or be pending execution assert!(matches!(final_status.status, OrderStatus::Filled | OrderStatus::Pending)); } #[tokio::test] async fn test_order_priority_time_precedence() { let mut order_book = OrderBook::new(Symbol::new("AAPL".to_string()).unwrap()); let base_time = chrono::Utc::now(); // Add first order at same price let order1 = Order { id: OrderId::from("ORDER-001"), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: base_time, status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; // Add second order at same price (later timestamp) let order2 = Order { id: OrderId::from("ORDER-002"), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(200)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: base_time + chrono::Duration::milliseconds(1), status: OrderStatus::New, client_id: ClientId::new("CLIENT-002".to_string()), }; order_book.add_order(order1).expect("Should add first order"); order_book.add_order(order2).expect("Should add second order"); // When matching, first order should have priority let orders_at_level = order_book.get_bid_orders_at_price(&Price::new(Decimal::from(150)).unwrap()) .expect("Should get orders at price"); assert_eq!(orders_at_level.len(), 2); assert_eq!(orders_at_level[0].id.value(), "ORDER-001"); // First by time assert_eq!(orders_at_level[1].id.value(), "ORDER-002"); // Second by time } #[tokio::test] async fn test_order_cancellation() { let mut matching_engine = MatchingEngine::new(); let order = Order { id: OrderId::from("CANCEL-TEST"), symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, order_type: OrderType::Limit, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create test quantity: {}", e)).unwrap(), price: Some(Price::new(Decimal::from(150)).unwrap()), time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), status: OrderStatus::New, client_id: ClientId::new("CLIENT-001".to_string()), }; // Submit order let submit_result = matching_engine.submit_order(order).await .expect("Should submit order"); assert_eq!(submit_result.status, OrderStatus::Pending); // Cancel order let cancel_result = matching_engine.cancel_order(&OrderId::from("CANCEL-TEST")).await .expect("Should cancel order"); assert_eq!(cancel_result.status, OrderStatus::Cancelled); // Verify order is no longer in book let final_status = matching_engine.get_order_status(&OrderId::from("CANCEL-TEST")).await .expect("Should get final status"); assert_eq!(final_status.status, OrderStatus::Cancelled); } #[tokio::test] async fn test_fill_generation() { let order_id = OrderId::from("FILL-TEST"); let trade_id = TradeId::new("TRADE-001".to_string()); let fill_id = FillId::new("FILL-001".to_string()); let fill = Fill { id: fill_id.clone(), order_id: order_id.clone(), trade_id, symbol: Symbol::new("AAPL".to_string()).unwrap(), side: Side::Buy, quantity: Quantity::new(Decimal::from(100)).map_err(|e| format!("Failed to create fill quantity: {}", e)).unwrap(), price: Price::new(Decimal::from(150)).unwrap(), timestamp: chrono::Utc::now(), commission: Some(Money::new(Decimal::from_str("1.50").unwrap(), Currency::USD)), }; assert_eq!(fill.id, fill_id); assert_eq!(fill.order_id, order_id); assert_eq!(fill.quantity.value(), Decimal::from(100)); assert_eq!(fill.price.value(), Decimal::from(150)); assert_eq!(fill.commission.as_ref().unwrap().amount, Decimal::from_str("1.50").unwrap()); } } // Production implementations for testing #[derive(Debug)] struct OrderBook { symbol: Symbol, bids: Vec, asks: Vec, } impl OrderBook { fn new(symbol: Symbol) -> Self { Self { symbol, bids: Vec::new(), asks: Vec::new(), } } fn symbol(&self) -> &Symbol { &self.symbol } fn add_order(&mut self, order: Order) -> Result<(), Box> { match order.side { Side::Buy => { self.bids.push(order); // Sort bids by price (highest first) then by time self.bids.sort_by(|a, b| { let price_cmp = b.price.as_ref().unwrap().value().cmp(&a.price.as_ref().unwrap().value()); if price_cmp == std::cmp::Ordering::Equal { a.timestamp.cmp(&b.timestamp) } else { price_cmp } }); }, Side::Sell => { self.asks.push(order); // Sort asks by price (lowest first) then by time self.asks.sort_by(|a, b| { let price_cmp = a.price.as_ref().unwrap().value().cmp(&b.price.as_ref().unwrap().value()); if price_cmp == std::cmp::Ordering::Equal { a.timestamp.cmp(&b.timestamp) } else { price_cmp } }); } } Ok(()) } fn best_bid(&self) -> Option<&Price> { self.bids.first().and_then(|order| order.price.as_ref()) } fn best_ask(&self) -> Option<&Price> { self.asks.first().and_then(|order| order.price.as_ref()) } fn total_bid_volume(&self) -> Quantity { let total: Decimal = self.bids.iter() .map(|order| order.quantity.value()) .sum(); Quantity::new(total).map_err(|e| format!("Failed to create quantity from total: {}", e)).unwrap() } fn total_ask_volume(&self) -> Quantity { let total: Decimal = self.asks.iter() .map(|order| order.quantity.value()) .sum(); Quantity::new(total).map_err(|e| format!("Failed to create quantity from total: {}", e)).unwrap() } fn get_bid_orders_at_price(&self, price: &Price) -> Result, Box> { Ok(self.bids.iter() .filter(|order| order.price.as_ref().unwrap().value() == price.value()) .collect()) } } #[derive(Debug)] struct MatchingEngine { order_books: HashMap, orders: HashMap, market_prices: HashMap, } #[derive(Debug, Clone)] struct OrderResult { status: OrderStatus, fills: Vec, filled_quantity: Quantity, remaining_quantity: Quantity, } #[derive(Debug, Clone)] struct OrderStatusResult { status: OrderStatus, filled_quantity: Quantity, remaining_quantity: Quantity, } impl MatchingEngine { fn new() -> Self { Self { order_books: HashMap::new(), orders: HashMap::new(), market_prices: HashMap::new(), } } async fn submit_order(&mut self, order: Order) -> Result> { let symbol = order.symbol.clone(); let order_id = order.id.clone(); let original_quantity = order.quantity.clone(); // Get or create order book if !self.order_books.contains_key(&symbol) { self.order_books.insert(symbol.clone(), OrderBook::new(symbol.clone())); } let mut result = OrderResult { status: OrderStatus::Pending, fills: Vec::new(), filled_quantity: Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap(), remaining_quantity: original_quantity.clone(), }; // Handle different order types match order.order_type { OrderType::Market => { // Try to fill immediately against best opposite side result = self.execute_market_order(order).await?; }, OrderType::Limit => { // Try to match, then add remainder to book result = self.execute_limit_order(order).await?; }, OrderType::StopLoss => { // Add to stop order book (simplified for testing) self.orders.insert(order_id, OrderStatus::Pending); result.status = OrderStatus::Pending; }, _ => { return Err("Unsupported order type".into()); } } Ok(result) } async fn execute_market_order(&mut self, order: Order) -> Result> { let order_book = self.order_books.get_mut(&order.symbol).unwrap(); let mut result = OrderResult { status: OrderStatus::Rejected, fills: Vec::new(), filled_quantity: Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap(), remaining_quantity: order.quantity.clone(), }; // Simple market order execution - match against best price match order.side { Side::Buy => { if let Some(best_ask) = order_book.best_ask() { let fill = Fill { id: FillId::new(format!("FILL-{}", order.id.value())), order_id: order.id.clone(), trade_id: TradeId::new(format!("TRADE-{}", order.id.value())), symbol: order.symbol.clone(), side: order.side, quantity: order.quantity.clone(), price: best_ask.clone(), timestamp: chrono::Utc::now(), commission: None, }; result.fills.push(fill); result.filled_quantity = order.quantity.clone(); result.remaining_quantity = Quantity::new(Decimal::ZERO).unwrap(); result.status = OrderStatus::Filled; } }, Side::Sell => { if let Some(best_bid) = order_book.best_bid() { let fill = Fill { id: FillId::new(format!("FILL-{}", order.id.value())), order_id: order.id.clone(), trade_id: TradeId::new(format!("TRADE-{}", order.id.value())), symbol: order.symbol.clone(), side: order.side, quantity: order.quantity.clone(), price: best_bid.clone(), timestamp: chrono::Utc::now(), commission: None, }; result.fills.push(fill); result.filled_quantity = order.quantity.clone(); result.remaining_quantity = Quantity::new(Decimal::ZERO).unwrap(); result.status = OrderStatus::Filled; } } } Ok(result) } async fn execute_limit_order(&mut self, order: Order) -> Result> { let order_book = self.order_books.get_mut(&order.symbol).unwrap(); let mut result = OrderResult { status: OrderStatus::Pending, fills: Vec::new(), filled_quantity: Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap(), remaining_quantity: order.quantity.clone(), }; // Check for immediate matching let can_match = match order.side { Side::Buy => { order_book.best_ask() .map(|ask_price| order.price.as_ref().unwrap().value() >= ask_price.value()) .unwrap_or(false) }, Side::Sell => { order_book.best_bid() .map(|bid_price| order.price.as_ref().unwrap().value() <= bid_price.value()) .unwrap_or(false) } }; if can_match { // Execute against opposite side let opposite_price = match order.side { Side::Buy => order_book.best_ask().unwrap().clone(), Side::Sell => order_book.best_bid().unwrap().clone(), }; let fill = Fill { id: FillId::new(format!("FILL-{}", order.id.value())), order_id: order.id.clone(), trade_id: TradeId::new(format!("TRADE-{}", order.id.value())), symbol: order.symbol.clone(), side: order.side, quantity: order.quantity.clone(), price: opposite_price, timestamp: chrono::Utc::now(), commission: None, }; result.fills.push(fill); result.filled_quantity = order.quantity.clone(); result.remaining_quantity = Quantity::new(Decimal::ZERO).unwrap(); result.status = OrderStatus::Filled; } else { // Add to order book order_book.add_order(order.clone())?; self.orders.insert(order.id.clone(), OrderStatus::Pending); } Ok(result) } async fn cancel_order(&mut self, order_id: &OrderId) -> Result> { self.orders.insert(order_id.clone(), OrderStatus::Cancelled); Ok(OrderResult { status: OrderStatus::Cancelled, fills: Vec::new(), filled_quantity: Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap(), remaining_quantity: Quantity::new(Decimal::ZERO).map_err(|e| format!("Failed to create zero quantity: {}", e)).unwrap(), }) } async fn get_order_status(&self, order_id: &OrderId) -> Result> { let status = self.orders.get(order_id).unwrap_or(&OrderStatus::Unknown); Ok(OrderStatusResult { status: status.clone(), filled_quantity: Quantity::new(Decimal::from(200)).map_err(|e| format!("Failed to create mock quantity: {}", e)).unwrap(), // Mock data remaining_quantity: Quantity::new(Decimal::from(300)).map_err(|e| format!("Failed to create mock quantity: {}", e)).unwrap(), // Mock data }) } async fn update_market_price(&mut self, symbol: &Symbol, price: Price) -> Result<(), Box> { self.market_prices.insert(symbol.clone(), price); // In real implementation, this would trigger stop orders Ok(()) } }