#![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, )] //! Integration test for real IBKR TWS/Gateway connectivity. //! //! These tests require a running IB Gateway or TWS on localhost:4002 (paper trading port). //! All tests are `#[ignore]` by default so they never run in CI or casual `cargo test`. //! //! Run manually with: //! ```sh //! SQLX_OFFLINE=true cargo test -p trading_engine --features interactive-brokers \ //! -- ibkr_connectivity --ignored --nocapture //! ``` //! //! Environment variables: //! - `IBKR_ACCOUNT_ID` - IB paper trading account (default: `DU9600528`) //! - `IBKR_HOST` - Gateway host (default: `127.0.0.1`) //! - `IBKR_PORT` - Gateway port (default: `4002`) #[cfg(feature = "interactive-brokers")] mod ibkr_tests { use trading_engine::brokers::config::InteractiveBrokersConfig; use trading_engine::brokers::interactive_brokers::InteractiveBrokersClient; use trading_engine::trading::data_interface::BrokerInterface; use std::sync::atomic::{AtomicI32, Ordering}; /// Each test gets a unique client_id to avoid IB Gateway rejecting /// parallel connections (only one connection per client_id is allowed). static NEXT_CLIENT_ID: AtomicI32 = AtomicI32::new(100); fn paper_trading_config() -> InteractiveBrokersConfig { let client_id = NEXT_CLIENT_ID.fetch_add(1, Ordering::SeqCst); InteractiveBrokersConfig { enabled: true, account_id: Some( std::env::var("IBKR_ACCOUNT_ID").unwrap_or_else(|_| "DU9600528".to_owned()), ), host: std::env::var("IBKR_HOST").unwrap_or_else(|_| "127.0.0.1".to_owned()), port: std::env::var("IBKR_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(4002), client_id, } } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_connect_disconnect() { let config = paper_trading_config(); println!( "Connecting to IB Gateway at {}:{} (client_id={})", config.host, config.port, config.client_id ); let mut client = InteractiveBrokersClient::new(config); let result = client.connect().await; assert!(result.is_ok(), "Failed to connect: {:?}", result.err()); assert!(client.is_connected(), "Client should report connected"); let result = client.disconnect().await; assert!( result.is_ok(), "Failed to disconnect: {:?}", result.err() ); assert!( !client.is_connected(), "Client should report disconnected" ); } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_account_info() { let config = paper_trading_config(); let mut client = InteractiveBrokersClient::new(config); client .connect() .await .expect("Failed to connect to IB Gateway"); let info = client.get_account_info().await; assert!( info.is_ok(), "Failed to get account info: {:?}", info.err() ); let info = info.expect("already checked is_ok"); println!("Account info ({} fields):", info.len()); for (key, value) in &info { println!(" {} = {}", key, value); } assert!(!info.is_empty(), "Account info should not be empty"); client.disconnect().await.ok(); } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_positions() { let config = paper_trading_config(); let mut client = InteractiveBrokersClient::new(config); client .connect() .await .expect("Failed to connect to IB Gateway"); let positions = client.get_positions().await; assert!( positions.is_ok(), "Failed to get positions: {:?}", positions.err() ); let positions = positions.expect("already checked is_ok"); println!("Open positions: {}", positions.len()); for pos in &positions { println!(" {:?}", pos); } client.disconnect().await.ok(); } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_connection_status() { use trading_engine::trading::data_interface::BrokerConnectionStatus; let config = paper_trading_config(); let mut client = InteractiveBrokersClient::new(config); // Before connect assert_eq!( client.connection_status(), BrokerConnectionStatus::Disconnected, "Should be disconnected before connect()" ); client .connect() .await .expect("Failed to connect to IB Gateway"); // After connect assert_eq!( client.connection_status(), BrokerConnectionStatus::Connected, "Should be connected after connect()" ); // Broker name assert_eq!(client.broker_name(), "Interactive Brokers"); client.disconnect().await.ok(); } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_submit_and_cancel_paper_order() { use chrono::Utc; use common::{OrderSide, OrderStatus, OrderType, TimeInForce}; use rust_decimal::Decimal; use std::collections::HashMap; use trading_engine::trading_operations::TradingOrder; let config = paper_trading_config(); let mut client = InteractiveBrokersClient::new(config); client .connect() .await .expect("Failed to connect to IB Gateway"); // Create a limit BUY order far below market to avoid fills. // ES micro futures at an absurdly low price ensures no execution. let order = TradingOrder { id: common::OrderId::new(), symbol: "ES.FUT".to_owned(), side: OrderSide::Buy, order_type: OrderType::Limit, quantity: Decimal::from(1), price: Decimal::from(1000), // Far below market (~5500 at time of writing) time_in_force: TimeInForce::Day, account_id: None, metadata: { let mut m = HashMap::new(); m.insert("exchange".to_owned(), "CME".to_owned()); m.insert("currency".to_owned(), "USD".to_owned()); m }, created_at: Utc::now(), submitted_at: None, executed_at: None, status: OrderStatus::Created, fill_quantity: Decimal::ZERO, average_fill_price: None, }; let submit_result = client.submit_order(&order).await; assert!( submit_result.is_ok(), "Failed to submit paper order: {:?}", submit_result.err() ); let broker_order_id = submit_result.expect("already checked is_ok"); println!("Paper order submitted, broker_order_id={}", broker_order_id); // Give TWS a moment to process tokio::time::sleep(std::time::Duration::from_secs(1)).await; // Verify order status let status = client.get_order_status(&broker_order_id).await; println!("Order status: {:?}", status); // Cancel the order let cancel_result = client.cancel_order(&broker_order_id).await; assert!( cancel_result.is_ok(), "Failed to cancel paper order: {:?}", cancel_result.err() ); println!("Paper order {} cancelled", broker_order_id); client.disconnect().await.ok(); } #[tokio::test] #[ignore = "Requires running IB Gateway on localhost:4002"] async fn ibkr_connectivity_heartbeat() { let config = paper_trading_config(); let mut client = InteractiveBrokersClient::new(config); client .connect() .await .expect("Failed to connect to IB Gateway"); // Heartbeat should succeed while connected let result = client.send_heartbeat().await; assert!( result.is_ok(), "Heartbeat failed: {:?}", result.err() ); client.disconnect().await.ok(); } }