#![allow( clippy::tests_outside_test_module, clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::str_to_string, clippy::string_to_string, clippy::assertions_on_result_states, clippy::assertions_on_constants, clippy::let_underscore_must_use, clippy::use_debug, clippy::doc_markdown, clippy::shadow_unrelated, clippy::shadow_reuse, clippy::similar_names, clippy::clone_on_copy, clippy::get_unwrap, clippy::modulo_arithmetic, clippy::integer_division, clippy::non_ascii_literal, clippy::useless_vec, clippy::useless_format, clippy::wildcard_enum_match_arm, clippy::manual_range_contains, clippy::const_is_empty, clippy::needless_range_loop, clippy::field_reassign_with_default, clippy::items_after_test_module, clippy::missing_const_for_fn, unused_imports, unused_variables, unused_mut, unused_assignments, unused_comparisons, unused_must_use, dead_code, )] //! Concurrency and Race Condition Tests for Trading Engine //! //! Tests critical concurrent access patterns in OrderManager, PositionManager, //! and lockfree queue implementations to ensure thread safety and correct behavior //! under high contention. use chrono::Utc; use common::{OrderId, OrderSide, OrderStatus, OrderType, TimeInForce}; use rust_decimal::Decimal; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinSet; use trading_engine::trading::order_manager::OrderManager; use trading_engine::trading::position_manager::PositionManager; use trading_engine::trading_operations::{ExecutionResult, LiquidityFlag, TradingOrder}; // ============================================================================ // Helper Functions // ============================================================================ fn create_test_order(symbol: &str, quantity: &str, price: &str, side: OrderSide) -> TradingOrder { TradingOrder { id: OrderId::new(), symbol: symbol.to_string(), side, order_type: OrderType::Limit, quantity: Decimal::from_str(quantity).unwrap(), price: Decimal::from_str(price).unwrap(), time_in_force: TimeInForce::Day, account_id: None, metadata: HashMap::new(), created_at: Utc::now(), submitted_at: None, executed_at: None, status: OrderStatus::Created, fill_quantity: Decimal::ZERO, average_fill_price: None, } } fn create_test_execution( symbol: &str, quantity: Decimal, price: Decimal, side: OrderSide, ) -> ExecutionResult { ExecutionResult { order_id: OrderId::new(), symbol: symbol.to_string(), side, executed_quantity: quantity, execution_price: price, commission: Decimal::from_str("0.01").unwrap(), execution_time: Utc::now(), liquidity_flag: LiquidityFlag::Maker, } } // ============================================================================ // OrderManager Concurrency Tests // ============================================================================ #[cfg(test)] mod order_manager_concurrency { use super::*; #[tokio::test] async fn test_concurrent_order_submission() { let order_manager = Arc::new(OrderManager::new()); let mut join_set = JoinSet::new(); // Submit 100 orders concurrently from 10 tasks for task_id in 0..10 { let om = Arc::clone(&order_manager); join_set.spawn(async move { for i in 0..10 { let order = create_test_order( "AAPL", "100", &format!("150.{:02}", (task_id * 10 + i) % 100), OrderSide::Buy, ); om.validate_order(&order).await.expect("Validation failed"); om.add_order(order).await; } }); } // Wait for all tasks to complete while let Some(result) = join_set.join_next().await { result.expect("Task panicked"); } // Verify all orders were added (no duplicates, no lost updates) // Note: This tests that concurrent adds don't corrupt the HashMap let orders = order_manager.get_orders(None).await; assert_eq!(orders.len(), 100, "All 100 orders should be tracked"); } #[tokio::test] async fn test_concurrent_order_status_updates() { let order_manager = Arc::new(OrderManager::new()); // Add 50 orders let mut order_ids = Vec::new(); for i in 0..50 { let order = create_test_order("MSFT", "100", &format!("300.{:02}", i), OrderSide::Buy); let order_id = order.id; order_manager.add_order(order).await; order_ids.push(order_id); } // Update all order statuses concurrently let mut join_set = JoinSet::new(); for order_id in order_ids.iter() { let om = Arc::clone(&order_manager); let oid = *order_id; join_set.spawn(async move { om.update_order_status(&oid, OrderStatus::Submitted) .await .expect("Status update failed"); }); } while let Some(result) = join_set.join_next().await { result.expect("Task panicked"); } // Verify all orders have updated status for order_id in order_ids { let order = order_manager.get_order(&order_id).await.unwrap(); assert_eq!(order.status, OrderStatus::Submitted); } } #[tokio::test] async fn test_concurrent_read_write_orders() { let order_manager = Arc::new(OrderManager::new()); // Add initial orders let mut order_ids = Vec::new(); for i in 0..20 { let order = create_test_order("GOOGL", "50", &format!("2800.{:02}", i), OrderSide::Buy); let order_id = order.id; order_manager.add_order(order).await; order_ids.push(order_id); } let mut join_set = JoinSet::new(); // Writers: Update order statuses for order_id in order_ids.iter().take(10) { let om = Arc::clone(&order_manager); let oid = *order_id; join_set.spawn(async move { for _ in 0..5 { let _ = om .update_order_status(&oid, OrderStatus::PartiallyFilled) .await; tokio::time::sleep(Duration::from_micros(10)).await; } }); } // Readers: Query orders repeatedly for order_id in order_ids.iter().skip(10) { let om = Arc::clone(&order_manager); let oid = *order_id; join_set.spawn(async move { for _ in 0..10 { let _ = om.get_order(&oid).await; tokio::time::sleep(Duration::from_micros(5)).await; } }); } // Wait for all operations while let Some(result) = join_set.join_next().await { result.expect("Task panicked"); } } #[tokio::test] async fn test_duplicate_order_id_concurrent() { let order_manager = Arc::new(OrderManager::new()); let order = create_test_order("TSLA", "10", "250.00", OrderSide::Buy); let order_id = order.id; // Add the order once order_manager.add_order(order.clone()).await; let mut join_set = JoinSet::new(); // Try to add duplicate order from multiple tasks for _ in 0..5 { let om = Arc::clone(&order_manager); let mut dup_order = order.clone(); dup_order.id = order_id; // Force same ID join_set.spawn(async move { om.validate_order(&dup_order).await }); } let mut validation_failures = 0; while let Some(result) = join_set.join_next().await { if result.unwrap().is_err() { validation_failures += 1; } } // All duplicate attempts should fail validation assert!( validation_failures >= 4, "Duplicate detection should catch concurrent submissions" ); } } // ============================================================================ // PositionManager Concurrency Tests // ============================================================================ #[cfg(test)] mod position_manager_concurrency { use super::*; #[tokio::test] async fn test_concurrent_position_updates_same_symbol() { let pm = Arc::new(PositionManager::new()); let mut join_set = JoinSet::new(); // Execute 100 trades concurrently for the same symbol for i in 0..100 { let position_manager = Arc::clone(&pm); join_set.spawn(async move { let exec = create_test_execution( "AAPL", Decimal::from_str("10").unwrap(), Decimal::from_str(&format!("150.{:02}", i % 100)).unwrap(), OrderSide::Buy, ); position_manager.update_position(&exec) }); } // Collect results let mut success_count = 0; while let Some(result) = join_set.join_next().await { if result.unwrap().is_ok() { success_count += 1; } } assert_eq!(success_count, 100, "All position updates should succeed"); // Verify final position quantity (100 trades * 10 shares each) let position = pm.get_position("AAPL").unwrap(); assert_eq!( position.quantity, Decimal::from_str("1000").unwrap(), "Total quantity should be sum of all trades" ); } #[tokio::test] async fn test_concurrent_position_updates_different_symbols() { let pm = Arc::new(PositionManager::new()); let symbols = vec!["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"]; let mut join_set = JoinSet::new(); // Create 50 executions (10 per symbol) concurrently for symbol in symbols.iter() { for i in 0..10 { let position_manager = Arc::clone(&pm); let sym = symbol.to_string(); join_set.spawn(async move { let exec = create_test_execution( &sym, Decimal::from_str("10").unwrap(), Decimal::from_str(&format!("100.{:02}", i)).unwrap(), OrderSide::Buy, ); position_manager.update_position(&exec) }); } } // Wait for all updates while let Some(result) = join_set.join_next().await { result.expect("Task panicked").expect("Update failed"); } // Verify each symbol has correct position for symbol in symbols { let position = pm.get_position(symbol).unwrap(); assert_eq!( position.quantity, Decimal::from_str("100").unwrap(), "Each symbol should have 10 trades * 10 shares" ); } } #[tokio::test] async fn test_position_reversal_under_concurrency() { let pm = Arc::new(PositionManager::new()); // First establish a long position let initial_exec = create_test_execution( "AMD", Decimal::from_str("100").unwrap(), Decimal::from_str("120.00").unwrap(), OrderSide::Buy, ); pm.update_position(&initial_exec).unwrap(); // Now concurrently execute sells that reverse the position let mut join_set = JoinSet::new(); for i in 0..15 { let position_manager = Arc::clone(&pm); join_set.spawn(async move { let exec = create_test_execution( "AMD", Decimal::from_str("10").unwrap(), Decimal::from_str(&format!("121.{:02}", i)).unwrap(), OrderSide::Sell, ); position_manager.update_position(&exec) }); } while let Some(result) = join_set.join_next().await { result.expect("Task panicked").expect("Update failed"); } // Position should be net short: 100 - 150 = -50 let position = pm.get_position("AMD").unwrap(); assert_eq!( position.quantity, Decimal::from_str("-50").unwrap(), "Position should reverse from long to short" ); } #[tokio::test] async fn test_concurrent_read_write_positions() { let pm = Arc::new(PositionManager::new()); // Initialize position let exec = create_test_execution( "NVDA", Decimal::from_str("100").unwrap(), Decimal::from_str("500.00").unwrap(), OrderSide::Buy, ); pm.update_position(&exec).unwrap(); let mut join_set = JoinSet::new(); // Writers: Update positions for i in 0..20 { let position_manager = Arc::clone(&pm); join_set.spawn(async move { let exec = create_test_execution( "NVDA", Decimal::from_str("5").unwrap(), Decimal::from_str(&format!("501.{:02}", i)).unwrap(), OrderSide::Buy, ); position_manager.update_position(&exec) }); } // Readers: Query positions repeatedly for _ in 0..30 { let position_manager = Arc::clone(&pm); join_set.spawn(async move { for _ in 0..5 { let _ = position_manager.get_position("NVDA"); tokio::time::sleep(Duration::from_micros(10)).await; } Ok::<(), String>(()) }); } // Collect all results while let Some(result) = join_set.join_next().await { result.expect("Task panicked").expect("Operation failed"); } } #[tokio::test] async fn test_position_zero_crossing_concurrent() { let pm = Arc::new(PositionManager::new()); // Start with a position let initial = create_test_execution( "INTC", Decimal::from_str("50").unwrap(), Decimal::from_str("45.00").unwrap(), OrderSide::Buy, ); pm.update_position(&initial).unwrap(); let mut join_set = JoinSet::new(); // Concurrently execute sells that bring position to ~zero for i in 0..5 { let position_manager = Arc::clone(&pm); join_set.spawn(async move { let exec = create_test_execution( "INTC", Decimal::from_str("10").unwrap(), Decimal::from_str(&format!("45.{:02}", i)).unwrap(), OrderSide::Sell, ); position_manager.update_position(&exec) }); } while let Some(result) = join_set.join_next().await { result.expect("Task panicked").expect("Update failed"); } // Position should be zero or near-zero let position = pm.get_position("INTC").unwrap(); assert_eq!( position.quantity, Decimal::ZERO, "Position should be flat after equal buy/sell" ); } } // ============================================================================ // Edge Case Tests // ============================================================================ #[cfg(test)] mod edge_case_tests { use super::*; #[test] fn test_position_rounding_accumulation() { let pm = PositionManager::new(); // Execute many small trades with prices that might cause rounding issues for i in 0..1000 { let exec = create_test_execution( "TEST", Decimal::from_str("0.001").unwrap(), Decimal::from_str(&format!("100.{:03}", i % 1000)).unwrap(), OrderSide::Buy, ); pm.update_position(&exec).unwrap(); } let position = pm.get_position("TEST").unwrap(); // Verify total quantity is correct (no rounding drift) assert_eq!( position.quantity, Decimal::from_str("1.000").unwrap(), "Rounding errors should not accumulate" ); } #[tokio::test] async fn test_order_manager_empty_symbol_rejection() { let om = OrderManager::new(); let mut order = create_test_order("", "100", "150.00", OrderSide::Buy); order.symbol = String::new(); let result = om.validate_order(&order).await; assert!(result.is_err(), "Empty symbol should be rejected"); assert!(result.unwrap_err().contains("symbol")); } #[tokio::test] async fn test_order_manager_zero_quantity_rejection() { let om = OrderManager::new(); let mut order = create_test_order("AAPL", "0", "150.00", OrderSide::Buy); order.quantity = Decimal::ZERO; let result = om.validate_order(&order).await; assert!(result.is_err(), "Zero quantity should be rejected"); assert!(result.unwrap_err().contains("quantity")); } #[tokio::test] async fn test_order_manager_negative_quantity_rejection() { let om = OrderManager::new(); let mut order = create_test_order("AAPL", "-100", "150.00", OrderSide::Buy); order.quantity = Decimal::from_str("-100").unwrap(); let result = om.validate_order(&order).await; assert!(result.is_err(), "Negative quantity should be rejected"); } #[tokio::test] async fn test_order_manager_zero_price_limit_order_rejection() { let om = OrderManager::new(); let mut order = create_test_order("AAPL", "100", "0", OrderSide::Buy); order.order_type = OrderType::Limit; order.price = Decimal::ZERO; let result = om.validate_order(&order).await; assert!(result.is_err(), "Zero price limit order should be rejected"); assert!(result.unwrap_err().contains("price")); } #[tokio::test] async fn test_order_status_transition_invalid() { let om = Arc::new(OrderManager::new()); let order = create_test_order("AAPL", "100", "150.00", OrderSide::Buy); let order_id = order.id; om.add_order(order).await; // Fill the order om.update_order_status(&order_id, OrderStatus::Filled) .await .unwrap(); // Try to transition from Filled to Cancelled (invalid) let _result = om .update_order_status(&order_id, OrderStatus::Cancelled) .await; // Note: Current implementation doesn't validate transitions // This test documents current behavior and would catch if we add validation // In production, this should be rejected } #[test] fn test_position_large_quantity() { let pm = PositionManager::new(); // Test with very large position size let exec = create_test_execution( "BIGPOS", Decimal::from_str("1000000").unwrap(), Decimal::from_str("100.00").unwrap(), OrderSide::Buy, ); let result = pm.update_position(&exec); assert!(result.is_ok(), "Large position should be handled"); let position = pm.get_position("BIGPOS").unwrap(); assert_eq!(position.quantity, Decimal::from_str("1000000").unwrap()); } #[test] fn test_position_high_precision_price() { let pm = PositionManager::new(); // Test with high precision price (e.g., crypto) let exec = create_test_execution( "CRYPTO", Decimal::from_str("0.00123456").unwrap(), Decimal::from_str("45678.9012345").unwrap(), OrderSide::Buy, ); let result = pm.update_position(&exec); assert!(result.is_ok(), "High precision should be handled"); let position = pm.get_position("CRYPTO").unwrap(); assert_eq!(position.quantity, Decimal::from_str("0.00123456").unwrap()); } } // ============================================================================ // Error Recovery Tests // ============================================================================ #[cfg(test)] mod error_recovery_tests { use super::*; #[tokio::test] async fn test_order_manager_update_nonexistent_order() { let om = OrderManager::new(); let fake_id = OrderId::new(); let result = om.update_order_status(&fake_id, OrderStatus::Filled).await; assert!(result.is_err(), "Updating nonexistent order should fail"); assert!(result.unwrap_err().contains("not found")); } #[tokio::test] async fn test_order_manager_get_nonexistent_order() { let om = OrderManager::new(); let fake_id = OrderId::new(); let result = om.get_order(&fake_id).await; assert!( result.is_none(), "Getting nonexistent order should return None" ); } #[test] fn test_position_manager_get_nonexistent_position() { let pm = PositionManager::new(); let result = pm.get_position("NONEXISTENT"); assert!( result.is_none(), "Getting nonexistent position should return None" ); } #[test] fn test_position_manager_lock_error_recovery() { // This test verifies that lock errors are propagated correctly // In practice, lock poisoning is rare and usually indicates a panic let pm = PositionManager::new(); let exec = create_test_execution( "LOCKTEST", Decimal::from_str("100").unwrap(), Decimal::from_str("150.00").unwrap(), OrderSide::Buy, ); let result = pm.update_position(&exec); assert!(result.is_ok(), "Normal lock acquisition should succeed"); } #[tokio::test] async fn test_concurrent_operation_resilience() { // Test that the system remains functional even if some operations fail let om = Arc::new(OrderManager::new()); let mut join_set = JoinSet::new(); // Mix of valid and invalid operations for i in 0..20 { let order_manager = Arc::clone(&om); join_set.spawn(async move { if i % 3 == 0 { // Invalid order (empty symbol) let mut order = create_test_order("", "100", "150.00", OrderSide::Buy); order.symbol = String::new(); order_manager.validate_order(&order).await } else { // Valid order let order = create_test_order(&format!("SYM{}", i), "100", "150.00", OrderSide::Buy); order_manager.validate_order(&order).await?; order_manager.add_order(order).await; Ok(()) } }); } let mut success_count = 0; let mut failure_count = 0; while let Some(result) = join_set.join_next().await { match result.unwrap() { Ok(_) => success_count += 1, Err(_) => failure_count += 1, } } // Should have roughly 1/3 failures and 2/3 successes assert!(success_count > 0, "Some operations should succeed"); assert!(failure_count > 0, "Some operations should fail"); assert_eq!( success_count + failure_count, 20, "All operations should complete" ); } }