//! Interactive Brokers TWS Real Integration Validation Tests //! //! These tests validate the REAL Interactive Brokers TWS integration by testing: //! - TCP connection establishment to TWS/Gateway //! - FIX protocol message parsing and construction //! - Order submission and execution workflows //! - Position tracking and account data retrieval //! - Connection recovery and error handling //! //! NOTE: These tests are designed to gracefully handle connection failures //! in CI environments while validating real broker integration functionality. #![allow(unused_crate_dependencies)] use std::env; use std::time::Duration; use std::collections::HashMap; use tokio::time::timeout; use tracing::{info, warn, error}; use trading_engine::brokers::interactive_brokers::{InteractiveBrokersClient, IBConfig}; use trading_engine::brokers::config::InteractiveBrokersConfig; use trading_engine::trading::data_interface::{BrokerInterface, BrokerConnectionStatus}; use trading_engine::prelude::{TradingOrder, Side}; use trading_engine::trading_operations::OrderType; use common::TimeInForce; /// Helper function to create test IB configuration fn create_test_ib_config() -> InteractiveBrokersConfig { InteractiveBrokersConfig { enabled: true, host: env::var("FOXHUNT_IB_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), port: env::var("FOXHUNT_IB_PORT") .map(|p| p.parse().unwrap_or(7497)) .unwrap_or(7497), client_id: env::var("FOXHUNT_IB_CLIENT_ID") .map(|id| id.parse().unwrap_or(1)) .unwrap_or(1), account_id: env::var("FOXHUNT_IB_ACCOUNT_ID").ok(), connection_timeout_secs: 10, // Short timeout for testing request_timeout_secs: 5, heartbeat_interval_secs: 30, max_reconnect_attempts: 2, paper_trading: true, // Always use paper trading for tests } } /// Helper function to create test trading order fn create_test_order(symbol: &str, side: Side, quantity: i64, price: f64) -> TradingOrder { TradingOrder { id: OrderId::new(), symbol: Symbol::new(symbol.to_string()), side, quantity: Quantity::from_f64(quantity as f64).unwrap_or_default(), price: Price::from_f64(price).unwrap_or_default(), order_type: OrderType::Limit, time_in_force: TimeInForce::Day, timestamp: chrono::Utc::now(), metadata: HashMap::new(), } } #[tokio::test] async fn test_ib_client_creation() { let config = create_test_ib_config(); let client = InteractiveBrokersClient::new(config); // Verify initial state assert_eq!(client.broker_name(), "InteractiveBrokers_TWS"); assert_eq!(client.connection_status(), BrokerConnectionStatus::Disconnected); assert!(!client.is_connected()); info!("✅ IB client creation test passed"); } #[tokio::test] async fn test_ib_connection_attempt() { let config = create_test_ib_config(); let mut client = InteractiveBrokersClient::new(config.clone()); info!("🔄 Attempting connection to IB TWS at {}:{}", config.host, config.port); // Attempt connection with timeout let connection_result = timeout( Duration::from_secs(15), client.connect() ).await; match connection_result { Ok(Ok(())) => { info!("✅ Successfully connected to IB TWS!"); // Verify connection status assert!(client.is_connected()); assert_eq!(client.connection_status(), BrokerConnectionStatus::Connected); // Test heartbeat let heartbeat_result = client.send_heartbeat().await; match heartbeat_result { Ok(()) => info!("✅ Heartbeat successful"), Err(e) => warn!("⚠ïļ Heartbeat failed: {}", e), } // Get account info let account_info = client.get_account_info().await; match account_info { Ok(info) => { info!("✅ Account info retrieved:"); for (key, value) in info { info!(" {}: {}", key, value); } } Err(e) => warn!("⚠ïļ Failed to get account info: {}", e), } // Clean disconnection let disconnect_result = client.disconnect().await; match disconnect_result { Ok(()) => info!("✅ Disconnected cleanly"), Err(e) => warn!("⚠ïļ Disconnect error: {}", e), } } Ok(Err(e)) => { warn!("⚠ïļ IB connection failed (expected in CI): {}", e); info!(" This is normal if TWS/Gateway is not running"); // Verify we're still in disconnected state assert!(!client.is_connected()); assert_eq!(client.connection_status(), BrokerConnectionStatus::Disconnected); } Err(_) => { warn!("⚠ïļ IB connection timed out (expected in CI)"); info!(" This is normal if TWS/Gateway is not accessible"); } } info!("✅ IB connection test completed (graceful handling verified)"); } #[tokio::test] async fn test_ib_order_submission_workflow() { let config = create_test_ib_config(); let mut client = InteractiveBrokersClient::new(config); // Try to connect (may fail in CI) let connection_result = timeout( Duration::from_secs(10), client.connect() ).await; match connection_result { Ok(Ok(())) => { info!("✅ Connected to IB for order testing"); // Create test orders let test_orders = vec![ create_test_order("AAPL", Side::Buy, 100, 150.50), create_test_order("MSFT", Side::Sell, 50, 300.25), create_test_order("GOOGL", Side::Buy, 10, 2500.00), ]; for (i, order) in test_orders.into_iter().enumerate() { info!("🔄 Submitting test order {}: {} {} shares of {}", i + 1, order.side, order.quantity, order.symbol); let submit_result = client.submit_order(order).await; match submit_result { Ok(broker_order_id) => { info!("✅ Order submitted successfully: {}", broker_order_id); // Wait a moment for order processing tokio::time::sleep(Duration::from_millis(500)).await; // Check order status let status_result = client.get_order_status(&broker_order_id).await; match status_result { Ok(status) => { info!(" Order status: {:?}", status); } Err(e) => { warn!(" Failed to get order status: {}", e); } } // Test order cancellation let cancel_result = client.cancel_order(&broker_order_id).await; match cancel_result { Ok(()) => { info!("✅ Order cancelled successfully"); } Err(e) => { warn!("⚠ïļ Order cancellation failed: {}", e); } } } Err(e) => { warn!("⚠ïļ Order submission failed: {}", e); info!(" This may be normal if using demo account"); } } } // Test position retrieval let positions_result = client.get_positions().await; match positions_result { Ok(positions) => { info!("✅ Retrieved {} positions", positions.len()); for position in positions { info!(" Position: {} {} shares @ ${}", position.symbol, position.quantity, position.average_price); } } Err(e) => { warn!("⚠ïļ Failed to get positions: {}", e); } } // Test execution subscription let execution_result = client.subscribe_executions().await; match execution_result { Ok(mut rx) => { info!("✅ Execution subscription established"); // Wait briefly for any execution reports let timeout_result = timeout( Duration::from_secs(2), rx.recv() ).await; match timeout_result { Ok(Some(execution)) => { info!("✅ Received execution report: {:?}", execution); } Ok(None) => { info!(" Execution channel closed"); } Err(_) => { info!(" No executions received (normal for test)"); } } } Err(e) => { warn!("⚠ïļ Failed to subscribe to executions: {}", e); } } let _ = client.disconnect().await; } Ok(Err(e)) => { warn!("⚠ïļ Cannot test orders - IB not connected: {}", e); info!(" Testing order validation logic instead..."); // Test order validation without connection let test_order = create_test_order("AAPL", Side::Buy, 100, 150.50); let submit_result = client.submit_order(&test_order).await; // Should fail with "not connected" error match submit_result { Ok(_) => { error!("❌ Order unexpectedly succeeded without connection"); panic!("Order should fail when not connected"); } Err(e) => { info!("✅ Order properly failed when not connected: {}", e); assert!(e.to_string().to_lowercase().contains("not connected") || e.to_string().to_lowercase().contains("not available")); } } } Err(_) => { warn!("⚠ïļ IB connection timed out - testing offline validation"); // Test that orders fail properly when not connected let test_order = create_test_order("AAPL", Side::Buy, 100, 150.50); let submit_result = client.submit_order(&test_order).await; match submit_result { Ok(_) => { error!("❌ Order unexpectedly succeeded without connection"); } Err(e) => { info!("✅ Order properly failed when not connected: {}", e); } } } } info!("✅ IB order workflow test completed"); } #[tokio::test] async fn test_ib_order_modification() { let config = create_test_ib_config(); let mut client = InteractiveBrokersClient::new(config); // Try to connect let connection_result = timeout( Duration::from_secs(10), client.connect() ).await; if let Ok(Ok(())) = connection_result { info!("✅ Connected to IB for order modification testing"); // Submit an initial order let original_order = create_test_order("AAPL", Side::Buy, 100, 150.00); match client.submit_order(&original_order).await { Ok(broker_order_id) => { info!("✅ Original order submitted: {}", broker_order_id); // Wait for order to be processed tokio::time::sleep(Duration::from_millis(1000)).await; // Create modified order (different price and quantity) let modified_order = create_test_order("AAPL", Side::Buy, 150, 149.50); // Test order modification let modify_result = client.modify_order(&broker_order_id, &modified_order).await; match modify_result { Ok(()) => { info!("✅ Order modification successful"); // Verify the modification took effect let status_result = client.get_order_status(&broker_order_id).await; match status_result { Ok(status) => { info!(" Modified order status: {:?}", status); } Err(e) => { warn!(" Failed to get modified order status: {}", e); } } } Err(e) => { warn!("⚠ïļ Order modification failed: {}", e); info!(" This may be normal depending on order state"); } } // Clean up - cancel the order let _ = client.cancel_order(&broker_order_id).await; } Err(e) => { warn!("⚠ïļ Cannot test modification - order submission failed: {}", e); } } let _ = client.disconnect().await; } else { warn!("⚠ïļ Cannot test modification - IB not connected"); info!(" Testing modification validation without connection..."); // Test modification without connection let test_order = create_test_order("AAPL", Side::Buy, 100, 150.50); let modify_result = client.modify_order("fake_order_id", &test_order).await; match modify_result { Ok(()) => { error!("❌ Modification unexpectedly succeeded without connection"); } Err(e) => { info!("✅ Modification properly failed when not connected: {}", e); } } } info!("✅ IB order modification test completed"); } #[tokio::test] async fn test_ib_reconnection_handling() { let config = create_test_ib_config(); let client = InteractiveBrokersClient::new(config); info!("🔄 Testing IB reconnection handling"); // Test reconnection without initial connection let reconnect_result = client.reconnect().await; match reconnect_result { Ok(()) => { info!("✅ Reconnection succeeded"); // Verify connection state assert!(client.is_connected()); // Test reconnection while already connected let second_reconnect = client.reconnect().await; match second_reconnect { Ok(()) => { info!("✅ Second reconnection succeeded"); } Err(e) => { warn!("⚠ïļ Second reconnection failed: {}", e); } } // Test multiple rapid reconnections for i in 1..=3 { let rapid_reconnect = timeout( Duration::from_secs(5), client.reconnect() ).await; match rapid_reconnect { Ok(Ok(())) => { info!("✅ Rapid reconnection {} succeeded", i); } Ok(Err(e)) => { warn!("⚠ïļ Rapid reconnection {} failed: {}", i, e); } Err(_) => { warn!("⚠ïļ Rapid reconnection {} timed out", i); } } // Small delay between attempts tokio::time::sleep(Duration::from_millis(100)).await; } } Err(e) => { warn!("⚠ïļ Reconnection failed (expected in CI): {}", e); info!(" This is normal if TWS/Gateway is not running"); } } info!("✅ IB reconnection test completed"); } #[tokio::test] async fn test_ib_error_handling() { let config = create_test_ib_config(); let client = InteractiveBrokersClient::new(config); info!("🔄 Testing IB error handling"); // Test operations without connection let test_order = create_test_order("INVALID_SYMBOL", Side::Buy, 0, -1.0); // Test invalid order submission let submit_result = client.submit_order(&test_order).await; match submit_result { Ok(_) => { error!("❌ Invalid order unexpectedly succeeded"); } Err(e) => { info!("✅ Invalid order properly rejected: {}", e); } } // Test cancellation of non-existent order let cancel_result = client.cancel_order("non_existent_order").await; match cancel_result { Ok(()) => { warn!("⚠ïļ Cancellation of non-existent order unexpectedly succeeded"); } Err(e) => { info!("✅ Cancellation of non-existent order properly failed: {}", e); } } // Test getting status of non-existent order let status_result = client.get_order_status("non_existent_order").await; match status_result { Ok(status) => { warn!("⚠ïļ Got status for non-existent order: {:?}", status); } Err(e) => { info!("✅ Status check for non-existent order properly failed: {}", e); } } // Test operations with invalid configuration let mut invalid_config = create_test_ib_config(); invalid_config.port = 0; // Invalid port let invalid_client = InteractiveBrokersClient::new(invalid_config); let invalid_connect_result = timeout( Duration::from_secs(5), async move { let mut client = invalid_client; client.connect().await } ).await; match invalid_connect_result { Ok(Ok(())) => { error!("❌ Connection with invalid config unexpectedly succeeded"); } Ok(Err(e)) => { info!("✅ Connection with invalid config properly failed: {}", e); } Err(_) => { info!("✅ Connection with invalid config properly timed out"); } } info!("✅ IB error handling test completed"); } #[tokio::test] async fn test_ib_message_protocol_validation() { use core::brokers::brokers::interactive_brokers::{TWSMessageBuilder, TWSMessageParser, TWSMessageType}; info!("🔄 Testing IB TWS message protocol"); // Test message construction let heartbeat_msg = TWSMessageBuilder::new(TWSMessageType::Heartbeat) .add_field("test_field") .add_int(12345) .add_double(123.45) .add_bool(true) .build(); assert!(!heartbeat_msg.is_empty()); assert!(heartbeat_msg.len() > 4); // Should have length prefix info!("✅ Message construction test passed"); // Test order message construction let order_msg = TWSMessageBuilder::new(TWSMessageType::PlaceOrder) .add_int(1) // Order ID .add_field("AAPL") // Symbol .add_field("STK") // Security type .add_field("BUY") // Side .add_double(100.0) // Quantity .add_field("LMT") // Order type .add_double(150.50) // Price .build(); assert!(!order_msg.is_empty()); info!("✅ Order message construction test passed"); // Test message parsing let test_message = "1\x00AAPL\x00100\x00150.0"; let parse_result = TWSMessageParser::parse_message(test_message.as_bytes()); match parse_result { Ok(fields) => { assert_eq!(fields.len(), 4); assert_eq!(fields[0], "1"); assert_eq!(fields[1], "AAPL"); assert_eq!(fields[2], "100"); assert_eq!(fields[3], "150.0"); info!("✅ Message parsing test passed"); } Err(e) => { error!("❌ Message parsing failed: {}", e); panic!("Message parsing should succeed"); } } // Test message type parsing let msg_type = TWSMessageParser::parse_message_type(&["3".to_string()]); assert_eq!(msg_type, Some(3)); info!("✅ Message type parsing test passed"); info!("✅ IB message protocol validation completed"); } #[tokio::test] async fn test_ib_performance_characteristics() { let config = create_test_ib_config(); let client = InteractiveBrokersClient::new(config); info!("🔄 Testing IB performance characteristics"); // Test client creation performance let start = std::time::Instant::now(); let iterations = 100; for _ in 0..iterations { let test_config = create_test_ib_config(); let _test_client = InteractiveBrokersClient::new(test_config); } let creation_time = start.elapsed(); let avg_creation_time = creation_time / iterations; info!("✅ Client creation performance:"); info!(" {} iterations in {:?}", iterations, creation_time); info!(" Average: {:?} per client", avg_creation_time); // Should be very fast (under 1ms per creation) assert!(avg_creation_time < Duration::from_millis(1), "Client creation too slow: {:?}", avg_creation_time); // Test message construction performance let msg_start = std::time::Instant::now(); let msg_iterations = 1000; for i in 0..msg_iterations { let _msg = TWSMessageBuilder::new(TWSMessageType::PlaceOrder) .add_int(i) .add_field("AAPL") .add_field("STK") .add_field("BUY") .add_double(100.0) .add_field("LMT") .add_double(150.50) .build(); } let msg_time = msg_start.elapsed(); let avg_msg_time = msg_time / msg_iterations; info!("✅ Message construction performance:"); info!(" {} iterations in {:?}", msg_iterations, msg_time); info!(" Average: {:?} per message", avg_msg_time); // Should be very fast (under 10Ξs per message) assert!(avg_msg_time < Duration::from_micros(10), "Message construction too slow: {:?}", avg_msg_time); info!("✅ IB performance characteristics test completed"); } /// Integration test helper for IB configuration validation #[tokio::test] async fn test_ib_configuration_validation() { info!("🔄 Testing IB configuration validation"); // Test valid configuration let valid_config = create_test_ib_config(); let client = InteractiveBrokersClient::new(valid_config); assert_eq!(client.broker_name(), "InteractiveBrokersClient"); // Test configuration with different parameters let test_configs = vec![ // Different hosts ("localhost", 7497), ("127.0.0.1", 7497), ("demo.interactivebrokers.com", 4001), // Different ports ("127.0.0.1", 7496), // TWS Live ("127.0.0.1", 7497), // TWS Paper ("127.0.0.1", 4001), // Gateway Live ("127.0.0.1", 4002), // Gateway Paper ]; for (host, port) in test_configs { let mut config = create_test_ib_config(); config.host = host.to_string(); config.port = port; let test_client = InteractiveBrokersClient::new(config); assert_eq!(test_client.broker_name(), "InteractiveBrokersClient"); info!("✅ Configuration valid for {}:{}", host, port); } info!("✅ IB configuration validation completed"); }