//! Comprehensive Interactive Brokers TWS/Gateway Integration Tests //! //! Tests broker connectivity, order management, execution reporting, //! error recovery, and edge cases for Interactive Brokers integration. #![allow(unused_crate_dependencies)] use chrono::Utc; use common::{OrderId, OrderSide, OrderStatus, OrderType, Position, Symbol, TimeInForce}; use data::brokers::common::{ BrokerConnectionStatus, BrokerError, ExecutionReport, TradingOrder, }; use data::brokers::interactive_brokers::IBConfig; // Note: IBClient doesn't exist - InteractiveBrokersAdapter is the actual implementation // use data::brokers::interactive_brokers::{IBClient, IBConfig}; use rust_decimal::Decimal; use std::str::FromStr; use uuid::Uuid; mod test_helpers; // ============================================================================ // IBConfig Tests - Configuration Validation // ============================================================================ #[test] fn test_ib_config_default_values() { let config = IBConfig::default(); // Verify default configuration (respects environment variables) assert_eq!(config.host, test_helpers::expected_host()); assert_eq!(config.port, test_helpers::expected_port()); assert!(config.connection_timeout > 0); assert!(config.heartbeat_interval > 0); assert!(config.request_timeout > 0); } #[test] fn test_ib_config_paper_trading() { let config = test_helpers::test_ib_config_paper(); assert_eq!(config.port, test_helpers::expected_port()); assert!(config.account_id.starts_with("DU") || config.account_id.starts_with("U")); } #[test] fn test_ib_config_live_trading() { let config = test_helpers::test_ib_config_live(); assert_eq!(config.port, 7496); assert!(config.account_id.starts_with("U")); } #[test] fn test_ib_config_gateway() { let config = test_helpers::test_ib_config_gateway(); assert_eq!(config.port, 4001); } #[test] fn test_ib_config_client_id_validation() { // Test valid client ID range (0-32767) let valid_ids = vec![0, 1, 100, 1000, 32767]; for id in valid_ids { let config = IBConfig { client_id: id, ..IBConfig::default() }; assert!(config.client_id >= 0 && config.client_id <= 32767); } } #[test] fn test_ib_config_timeout_edge_cases() { let config = IBConfig { connection_timeout: 0, heartbeat_interval: 0, request_timeout: 0, ..IBConfig::default() }; // Should handle zero timeouts gracefully assert_eq!(config.connection_timeout, 0); } #[test] fn test_ib_config_serialization() { use serde_json; let config = IBConfig::default(); let json = serde_json::to_string(&config).unwrap(); let deserialized: IBConfig = serde_json::from_str(&json).unwrap(); assert_eq!(config.host, deserialized.host); assert_eq!(config.port, deserialized.port); assert_eq!(config.client_id, deserialized.client_id); } // ============================================================================ // TradingOrder Tests - Order Construction and Validation // ============================================================================ #[test] fn test_trading_order_market_order() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: 100.0, price: None, stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert_eq!(order.symbol, "AAPL".to_string()); assert!(matches!(order.side, OrderSide::Buy)); assert!(matches!(order.order_type, OrderType::Market)); assert!(order.price.is_none()); } #[test] fn test_trading_order_limit_order() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "TSLA".to_string(), side: OrderSide::Sell, order_type: OrderType::Limit, quantity: 50.0, price: Some(250.50), stop_price: None, time_in_force: TimeInForce::GoodTillCancel, client_order_id: Some("DU123456".to_string()), }; assert_eq!(order.symbol, "TSLA".to_string()); assert!(matches!(order.side, OrderSide::Sell)); assert!(matches!(order.order_type, OrderType::Limit)); assert!(order.price.is_some()); } #[test] fn test_trading_order_stop_order() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy, order_type: OrderType::Stop, quantity: 10.0, price: None, stop_price: Some(150.00), time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert!(matches!(order.order_type, OrderType::Stop)); assert!(order.stop_price.is_some()); } #[test] fn test_trading_order_time_in_force_variants() { let tif_variants = vec![ TimeInForce::Day, TimeInForce::GoodTillCancel, TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill, ]; for tif in tif_variants { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "SPY".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: 1.0, price: None, stop_price: None, time_in_force: tif, client_order_id: Some("DU123456".to_string()), }; assert!(!order.symbol.is_empty()); } } #[test] fn test_trading_order_quantity_edge_cases() { let quantities = vec![1.0, 0.01, 1000000.0]; for qty in quantities { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "BTC".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: qty, price: None, stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert!(order.quantity > 0.0); } } // ============================================================================ // ExecutionReport Tests - Trade Execution Reporting // ============================================================================ #[test] fn test_execution_report_filled() { let report = ExecutionReport { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 150.25, executed_quantity: 100.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 1.50, fee: 0.02, status: OrderStatus::Filled, }; assert!(matches!(report.status, OrderStatus::Filled)); assert_eq!(report.executed_quantity, 100.0); } #[test] fn test_execution_report_partial_fill() { let report = ExecutionReport { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 150.25, executed_quantity: 50.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 0.75, fee: 0.01, status: OrderStatus::PartiallyFilled, }; assert!(matches!(report.status, OrderStatus::PartiallyFilled)); assert!(report.executed_quantity < 100.0); } #[test] fn test_execution_report_rejected() { let report = ExecutionReport { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 0.0, executed_quantity: 0.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 0.0, fee: 0.0, status: OrderStatus::Rejected, }; assert!(matches!(report.status, OrderStatus::Rejected)); assert_eq!(report.executed_quantity, 0.0); } #[test] fn test_execution_report_cancelled() { let report = ExecutionReport { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 0.0, executed_quantity: 0.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 0.0, fee: 0.0, status: OrderStatus::Cancelled, }; assert!(matches!(report.status, OrderStatus::Cancelled)); } #[test] fn test_execution_report_commission_edge_cases() { let commissions = vec![0.0, 0.01, 1.00, 100.00]; for commission in commissions { let report = ExecutionReport { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 150.25, executed_quantity: 100.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission, fee: 0.02, status: OrderStatus::Filled, }; assert!(matches!(report.status, OrderStatus::Filled)); } } // ============================================================================ // BrokerConnectionStatus Tests - Connection State Management // ============================================================================ #[test] fn test_broker_connection_status_variants() { let statuses = vec![ BrokerConnectionStatus::Disconnected, BrokerConnectionStatus::Connecting, BrokerConnectionStatus::Connected, BrokerConnectionStatus::Reconnecting, BrokerConnectionStatus::Error("Connection failed".to_string()), ]; for status in statuses { let debug_str = format!("{:?}", status); assert!(!debug_str.is_empty()); } } #[test] fn test_broker_connection_status_transitions() { let mut status = BrokerConnectionStatus::Disconnected; // Simulate state transitions status = BrokerConnectionStatus::Connecting; assert!(matches!(status, BrokerConnectionStatus::Connecting)); status = BrokerConnectionStatus::Connected; assert!(matches!(status, BrokerConnectionStatus::Connected)); status = BrokerConnectionStatus::Reconnecting; assert!(matches!(status, BrokerConnectionStatus::Reconnecting)); status = BrokerConnectionStatus::Error("Timeout".to_string()); assert!(matches!(status, BrokerConnectionStatus::Error(_))); status = BrokerConnectionStatus::Disconnected; assert!(matches!(status, BrokerConnectionStatus::Disconnected)); } // ============================================================================ // BrokerError Tests - Error Handling // ============================================================================ #[test] fn test_broker_error_variants() { let errors = vec![ BrokerError::ConnectionFailed("Timeout".to_string()), BrokerError::Authentication("Invalid credentials".to_string()), BrokerError::Order("Insufficient margin".to_string()), BrokerError::Order("Missing price".to_string()), BrokerError::MarketData("Symbol XYZ not found".to_string()), BrokerError::Timeout("Rate limit exceeded".to_string()), BrokerError::ProtocolError("Server error".to_string()), ]; for error in errors { let debug_str = format!("{:?}", error); assert!(!debug_str.is_empty()); } } #[test] fn test_broker_error_display() { let error = BrokerError::Order("Test rejection".to_string()); let display_str = format!("{}", error); assert!(display_str.contains("Test rejection") || !display_str.is_empty()); } // ============================================================================ // Position Tests - Position Management // ============================================================================ #[test] fn test_position_long() { let position = Position { id: Uuid::new_v4(), symbol: Symbol::from("AAPL").to_string(), quantity: Decimal::from_str("100").unwrap(), avg_price: Decimal::from_str("150.00").unwrap(), avg_cost: Decimal::from_str("150.00").unwrap(), basis: Decimal::from_str("15000.00").unwrap(), average_price: Decimal::from_str("150.00").unwrap(), market_value: Decimal::from_str("15500.00").unwrap(), unrealized_pnl: Decimal::from_str("500.00").unwrap(), realized_pnl: Decimal::ZERO, created_at: Utc::now(), updated_at: Utc::now(), last_updated: Utc::now(), current_price: Some(Decimal::from_str("155.00").unwrap()), notional_value: Decimal::from_str("15500.00").unwrap(), margin_requirement: Decimal::ZERO, }; assert!(position.quantity > Decimal::ZERO); assert!(position.unrealized_pnl > Decimal::ZERO); } #[test] fn test_position_short() { let position = Position { id: Uuid::new_v4(), symbol: Symbol::from("TSLA").to_string(), quantity: Decimal::from_str("-50").unwrap(), avg_price: Decimal::from_str("250.00").unwrap(), avg_cost: Decimal::from_str("250.00").unwrap(), basis: Decimal::from_str("-12500.00").unwrap(), average_price: Decimal::from_str("250.00").unwrap(), market_value: Decimal::from_str("-12250.00").unwrap(), unrealized_pnl: Decimal::from_str("250.00").unwrap(), realized_pnl: Decimal::ZERO, created_at: Utc::now(), updated_at: Utc::now(), last_updated: Utc::now(), current_price: Some(Decimal::from_str("245.00").unwrap()), notional_value: Decimal::from_str("12250.00").unwrap(), margin_requirement: Decimal::ZERO, }; assert!(position.quantity < Decimal::ZERO); assert!(position.unrealized_pnl > Decimal::ZERO); } #[test] fn test_position_flat() { let position = Position { id: Uuid::new_v4(), symbol: Symbol::from("SPY").to_string(), quantity: Decimal::ZERO, avg_price: Decimal::ZERO, avg_cost: Decimal::ZERO, basis: Decimal::ZERO, average_price: Decimal::ZERO, market_value: Decimal::ZERO, unrealized_pnl: Decimal::ZERO, realized_pnl: Decimal::from_str("1000.00").unwrap(), created_at: Utc::now(), updated_at: Utc::now(), last_updated: Utc::now(), current_price: Some(Decimal::from_str("450.00").unwrap()), notional_value: Decimal::ZERO, margin_requirement: Decimal::ZERO, }; assert_eq!(position.quantity, Decimal::ZERO); assert_eq!(position.unrealized_pnl, Decimal::ZERO); } // ============================================================================ // Error Recovery Tests // ============================================================================ #[test] fn test_reconnection_backoff_strategy() { let base_delay_ms = 1000; let max_attempts = 5; for attempt in 0..max_attempts { let delay = base_delay_ms * 2_u64.pow(attempt); let capped_delay = delay.min(30_000); // Cap at 30 seconds assert!(capped_delay >= base_delay_ms); assert!(capped_delay <= 30_000); } } #[test] fn test_max_reconnect_attempts_enforcement() { let config = IBConfig { max_reconnect_attempts: 3, ..IBConfig::default() }; let mut attempts = 0; loop { attempts += 1; if attempts > config.max_reconnect_attempts { break; } } assert_eq!(attempts, config.max_reconnect_attempts + 1); } // ============================================================================ // Order Validation Tests // ============================================================================ #[test] fn test_order_validation_missing_price_for_limit() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, order_type: OrderType::Limit, quantity: 100.0, price: None, // Should have price stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; // Limit orders require price assert!(order.price.is_none()); assert!(matches!(order.order_type, OrderType::Limit)); } #[test] fn test_order_validation_zero_quantity() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: 0.0, // Invalid price: None, stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert_eq!(order.quantity, 0.0); } #[test] fn test_order_validation_empty_symbol() { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: 100.0, price: None, stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert!(order.symbol.is_empty()); } // ============================================================================ // Message Protocol Tests // ============================================================================ #[test] fn test_tws_message_encoding() { // Test TWS message field encoding let fields = vec!["1", "AAPL", "BUY", "100", "MKT"]; let encoded = fields.join("\0"); assert!(encoded.contains("AAPL")); assert!(encoded.contains("BUY")); } #[test] fn test_tws_message_decoding() { // Test TWS message field decoding let message = "8\01\0AAPL\0100\0150.25\0"; let fields: Vec<&str> = message.split('\0').collect(); assert!(fields.len() > 0); assert!(fields.contains(&"AAPL")); } // ============================================================================ // Concurrent Operations Tests // ============================================================================ #[tokio::test] async fn test_concurrent_order_submissions() { use tokio::task; let handles: Vec<_> = (0..10) .map(|i| { task::spawn(async move { let order = TradingOrder { order_id: OrderId::new().to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, order_type: OrderType::Market, quantity: (i + 1) as f64, price: None, stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; order }) }) .collect(); for handle in handles { let order = handle.await.unwrap(); assert!(order.quantity > 0.0); } } // ============================================================================ // Integration Scenario Tests // ============================================================================ #[test] fn test_order_lifecycle_scenario() { // Simulate complete order lifecycle let order_id = OrderId::new(); // 1. Order created let order = TradingOrder { order_id: order_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, order_type: OrderType::Limit, quantity: 100.0, price: Some(150.00), stop_price: None, time_in_force: TimeInForce::Day, client_order_id: Some("DU123456".to_string()), }; assert!(matches!(order.order_type, OrderType::Limit)); // 2. Order acknowledged (pending status) let ack_report = ExecutionReport { order_id: order_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 0.0, executed_quantity: 0.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 0.0, fee: 0.0, status: OrderStatus::Pending, }; assert!(matches!(ack_report.status, OrderStatus::Pending)); // 3. Partial fill let partial_report = ExecutionReport { order_id: order_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 150.00, executed_quantity: 50.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 0.75, fee: 0.01, status: OrderStatus::PartiallyFilled, }; assert!(matches!(partial_report.status, OrderStatus::PartiallyFilled)); // 4. Complete fill let fill_report = ExecutionReport { order_id: order_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy, executed_price: 150.00, executed_quantity: 100.0, timestamp_ns: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64, broker_id: "IB123456".to_string(), commission: 1.50, fee: 0.02, status: OrderStatus::Filled, }; assert!(matches!(fill_report.status, OrderStatus::Filled)); assert_eq!( fill_report.executed_quantity, order.quantity.to_string().parse::().unwrap() ); }