Files
foxhunt/tests/integration/broker_integration_tests.rs
jgrusewski 6093eac7bf 🔧 Tonic 0.14 Upgrade: Auto-generated and build system changes
Wave 64-65 cleanup: Proto regeneration and build system updates from Tonic 0.12→0.14 upgrade

Files updated:
- Cargo.lock: Dependency resolution for Tonic 0.14.2
- All build.rs: Updated for tonic-prost-build
- Proto files: Regenerated with tonic-prost 0.14
- Examples/tests: Updated for new gRPC API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 07:34:26 +02:00

649 lines
24 KiB
Rust

//! Broker Integration Tests
//!
//! Comprehensive test suite for real broker connectivity and trading operations.
//! Tests Interactive Brokers TWS, ICMarkets FIX, and broker failover scenarios.
#![allow(unused_crate_dependencies)]
use std::time::{Duration, Instant};
use tokio::time::timeout;
// Note: These broker types should be imported from actual crate when available
// use data::brokers::{InteractiveBrokers, ICMarkets, BrokerManager};
use risk::{RiskEngine, PositionTracker};
// Simple test configuration for this file
#[derive(Debug, Clone)]
struct UnifiedTestConfig {
initial_capital: common::prelude::Decimal,
enable_logging: bool,
}
fn create_test_config() -> UnifiedTestConfig {
UnifiedTestConfig {
initial_capital: common::prelude::Decimal::from(100000),
enable_logging: false,
}
}
/// Configuration for broker testing
#[derive(Debug, Clone)]
pub struct BrokerTestConfig {
pub connection_timeout: Duration,
pub order_execution_timeout: Duration,
pub max_order_latency: Duration,
pub max_position_sync_time: Duration,
pub test_symbol: Symbol,
pub test_quantity: Quantity,
pub enable_real_trading: bool,
pub demo_mode: bool,
}
impl Default for BrokerTestConfig {
fn default() -> Self {
Self {
connection_timeout: Duration::from_secs(30),
order_execution_timeout: Duration::from_secs(10),
max_order_latency: Duration::from_millis(100),
max_position_sync_time: Duration::from_secs(5),
test_symbol: Symbol::new("EURUSD").unwrap(),
test_quantity: Quantity::new(1000).unwrap(),
enable_real_trading: false, // Safety: disable real trading by default
demo_mode: true,
}
}
}
/// Broker connection status
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionStatus {
Connected,
Disconnected,
Connecting,
Error(String),
}
/// Order execution result
#[derive(Debug, Clone)]
pub struct OrderExecutionResult {
pub order_id: String,
pub execution_time: Duration,
pub filled_quantity: Quantity,
pub average_price: Price,
pub status: OrderStatus,
pub broker_fees: Price,
}
/// Position synchronization result
#[derive(Debug, Clone)]
pub struct PositionSyncResult {
pub symbol: Symbol,
pub broker_position: Quantity,
pub system_position: Quantity,
pub sync_time: Duration,
pub discrepancy: Quantity,
}
/// Broker test suite
pub struct BrokerTestSuite {
config: BrokerTestConfig,
broker_manager: BrokerManager,
risk_engine: RiskEngine,
position_tracker: PositionTracker,
}
impl BrokerTestSuite {
pub async fn new(config: BrokerTestConfig) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let unified_config = create_test_config();
let broker_manager = BrokerManager::new(unified_config.broker.clone()).await?;
let risk_engine = RiskEngine::new(unified_config.risk.clone()).await?;
let position_tracker = PositionTracker::new().await?;
Ok(Self {
config,
broker_manager,
risk_engine,
position_tracker,
})
}
pub async fn test_broker_connection(&mut self, broker_name: &str) -> Result<ConnectionStatus, Box<dyn std::error::Error + Send + Sync>> {
let connection_future = self.broker_manager.connect(broker_name);
let result = timeout(self.config.connection_timeout, connection_future).await;
match result {
Ok(Ok(_)) => {
// Verify connection by requesting account info
let account_info = self.broker_manager.get_account_info(broker_name).await?;
if account_info.is_connected {
Ok(ConnectionStatus::Connected)
} else {
Ok(ConnectionStatus::Disconnected)
}
}
Ok(Err(e)) => Ok(ConnectionStatus::Error(e.to_string())),
Err(_) => Ok(ConnectionStatus::Error("Connection timeout".to_string())),
}
}
pub async fn test_order_execution(
&mut self,
broker_name: &str,
order: &Order
) -> Result<OrderExecutionResult, Box<dyn std::error::Error + Send + Sync>> {
let start_time = Instant::now();
// Submit order through broker
let execution_future = self.broker_manager.submit_order(broker_name, order);
let execution_result = timeout(self.config.order_execution_timeout, execution_future).await??;
let execution_time = start_time.elapsed();
// Validate execution latency
assert!(
execution_time <= self.config.max_order_latency,
"Order execution latency {}ms exceeds maximum {}ms",
execution_time.as_millis(),
self.config.max_order_latency.as_millis()
);
Ok(OrderExecutionResult {
order_id: execution_result.order_id,
execution_time,
filled_quantity: execution_result.filled_quantity,
average_price: execution_result.average_price,
status: execution_result.status,
broker_fees: execution_result.fees,
})
}
pub async fn test_position_synchronization(
&mut self,
broker_name: &str,
symbol: &Symbol
) -> Result<PositionSyncResult, Box<dyn std::error::Error + Send + Sync>> {
let start_time = Instant::now();
// Get broker position
let broker_position = self.broker_manager.get_position(broker_name, symbol).await?;
// Get system position
let system_position = self.position_tracker.get_position(symbol).await?;
let sync_time = start_time.elapsed();
// Calculate discrepancy
let discrepancy = Quantity::new(
(broker_position.value() - system_position.value()).abs()
)?;
// Validate sync time
assert!(
sync_time <= self.config.max_position_sync_time,
"Position sync time {}ms exceeds maximum {}ms",
sync_time.as_millis(),
self.config.max_position_sync_time.as_millis()
);
Ok(PositionSyncResult {
symbol: symbol.clone(),
broker_position,
system_position,
sync_time,
discrepancy,
})
}
pub async fn test_market_data_feed(&mut self, broker_name: &str) -> Result<Duration, Box<dyn std::error::Error + Send + Sync>> {
let start_time = Instant::now();
// Subscribe to market data
self.broker_manager.subscribe_market_data(broker_name, &self.config.test_symbol).await?;
// Wait for first market data update
let market_data = self.broker_manager.get_market_data(&self.config.test_symbol).await?;
let latency = start_time.elapsed();
// Validate market data quality
assert!(market_data.bid > Price::zero(), "Invalid bid price");
assert!(market_data.ask > Price::zero(), "Invalid ask price");
assert!(market_data.ask >= market_data.bid, "Ask price below bid price");
Ok(latency)
}
async fn create_test_order(&self, side: OrderSide) -> Result<Order, Box<dyn std::error::Error + Send + Sync>> {
let current_price = self.broker_manager.get_current_price(&self.config.test_symbol).await?;
// Create order slightly away from market to avoid immediate execution in demo
let order_price = match side {
OrderSide::Buy => current_price - Price::new(0.0001)?,
OrderSide::Sell => current_price + Price::new(0.0001)?,
};
Ok(Order {
id: format!("test_order_{}", chrono::Utc::now().timestamp_nanos()),
symbol: self.config.test_symbol.clone(),
side,
order_type: OrderType::Limit,
quantity: self.config.test_quantity,
price: Some(order_price),
stop_price: None,
time_in_force: TimeInForce::GoodTillCancel,
created_at: std::time::SystemTime::now(),
updated_at: std::time::SystemTime::now(),
status: OrderStatus::PendingNew,
})
}
}
#[tokio::test]
async fn test_interactive_brokers_connection() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config).await?;
let connection_status = test_suite.test_broker_connection("interactive_brokers").await?;
match connection_status {
ConnectionStatus::Connected => {
println!("✅ Interactive Brokers: Connected successfully");
}
ConnectionStatus::Error(msg) if msg.contains("TWS not running") => {
println!("⚠️ Interactive Brokers: TWS not running (expected in CI)");
return Ok(()); // Skip test if TWS not available
}
other => {
panic!("Interactive Brokers connection failed: {:?}", other);
}
}
Ok(())
}
#[tokio::test]
async fn test_icmarkets_connection() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config).await?;
let connection_status = test_suite.test_broker_connection("icmarkets").await?;
match connection_status {
ConnectionStatus::Connected => {
println!("✅ ICMarkets: Connected successfully");
}
ConnectionStatus::Error(msg) if msg.contains("credentials") => {
println!("⚠️ ICMarkets: No credentials configured (expected in CI)");
return Ok(()); // Skip test if credentials not available
}
other => {
panic!("ICMarkets connection failed: {:?}", other);
}
}
Ok(())
}
#[tokio::test]
async fn test_order_lifecycle_interactive_brokers() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut config = BrokerTestConfig::default();
config.demo_mode = true; // Ensure demo mode for safety
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
// Skip if broker not available
let connection_status = test_suite.test_broker_connection("interactive_brokers").await?;
if connection_status != ConnectionStatus::Connected {
println!("⚠️ Skipping order test - Interactive Brokers not connected");
return Ok(());
}
// Test buy order
let buy_order = test_suite.create_test_order(OrderSide::Buy).await?;
let buy_result = test_suite.test_order_execution("interactive_brokers", &buy_order).await?;
assert!(
buy_result.execution_time <= config.max_order_latency,
"Buy order execution time {}ms exceeds limit",
buy_result.execution_time.as_millis()
);
// Test sell order
let sell_order = test_suite.create_test_order(OrderSide::Sell).await?;
let sell_result = test_suite.test_order_execution("interactive_brokers", &sell_order).await?;
assert!(
sell_result.execution_time <= config.max_order_latency,
"Sell order execution time {}ms exceeds limit",
sell_result.execution_time.as_millis()
);
println!("✅ Interactive Brokers Order Lifecycle: Buy={}ms, Sell={}ms",
buy_result.execution_time.as_millis(),
sell_result.execution_time.as_millis());
Ok(())
}
#[tokio::test]
async fn test_order_lifecycle_icmarkets() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut config = BrokerTestConfig::default();
config.demo_mode = true; // Ensure demo mode for safety
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
// Skip if broker not available
let connection_status = test_suite.test_broker_connection("icmarkets").await?;
if connection_status != ConnectionStatus::Connected {
println!("⚠️ Skipping order test - ICMarkets not connected");
return Ok(());
}
// Test market order execution speed
let market_order = Order {
id: format!("market_test_{}", chrono::Utc::now().timestamp_nanos()),
symbol: config.test_symbol.clone(),
side: OrderSide::Buy,
order_type: OrderType::Market,
quantity: config.test_quantity,
price: None,
stop_price: None,
time_in_force: TimeInForce::ImmediateOrCancel,
created_at: std::time::SystemTime::now(),
updated_at: std::time::SystemTime::now(),
status: OrderStatus::PendingNew,
};
let result = test_suite.test_order_execution("icmarkets", &market_order).await?;
assert!(
result.execution_time <= Duration::from_millis(50),
"ICMarkets market order too slow: {}ms",
result.execution_time.as_millis()
);
println!("✅ ICMarkets Order Execution: {}ms market order",
result.execution_time.as_millis());
Ok(())
}
#[tokio::test]
async fn test_position_synchronization() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
let brokers = vec!["interactive_brokers", "icmarkets"];
for broker_name in brokers {
let connection_status = test_suite.test_broker_connection(broker_name).await?;
if connection_status != ConnectionStatus::Connected {
println!("⚠️ Skipping position sync for {} - not connected", broker_name);
continue;
}
let sync_result = test_suite.test_position_synchronization(broker_name, &config.test_symbol).await?;
assert!(
sync_result.sync_time <= config.max_position_sync_time,
"{} position sync too slow: {}ms",
broker_name, sync_result.sync_time.as_millis()
);
// Allow small discrepancies (rounding, different precision)
assert!(
sync_result.discrepancy.value().abs() <= 1,
"{} position discrepancy too large: {} vs {}",
broker_name, sync_result.broker_position.value(), sync_result.system_position.value()
);
println!("{}: Position sync {}ms, discrepancy={}",
broker_name, sync_result.sync_time.as_millis(), sync_result.discrepancy.value());
}
Ok(())
}
#[tokio::test]
async fn test_market_data_feeds() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config).await?;
let brokers = vec!["interactive_brokers", "icmarkets"];
for broker_name in brokers {
let connection_status = test_suite.test_broker_connection(broker_name).await?;
if connection_status != ConnectionStatus::Connected {
println!("⚠️ Skipping market data test for {} - not connected", broker_name);
continue;
}
let data_latency = test_suite.test_market_data_feed(broker_name).await?;
assert!(
data_latency <= Duration::from_millis(500),
"{} market data latency too high: {}ms",
broker_name, data_latency.as_millis()
);
println!("{}: Market data latency {}ms",
broker_name, data_latency.as_millis());
}
Ok(())
}
#[tokio::test]
async fn test_broker_failover() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
// Test primary broker
let primary_status = test_suite.test_broker_connection("interactive_brokers").await?;
let backup_status = test_suite.test_broker_connection("icmarkets").await?;
if primary_status == ConnectionStatus::Connected {
println!("✅ Primary broker (Interactive Brokers) available");
// Test failover scenario
test_suite.broker_manager.simulate_disconnect("interactive_brokers").await?;
// Verify automatic failover to backup
let order = test_suite.create_test_order(OrderSide::Buy).await?;
let result = test_suite.broker_manager.submit_order_with_failover(&order).await?;
assert!(result.broker_used == "icmarkets" || backup_status != ConnectionStatus::Connected,
"Failover should use backup broker when primary unavailable");
println!("✅ Broker failover working: Primary → Backup");
} else if backup_status == ConnectionStatus::Connected {
println!("✅ Backup broker (ICMarkets) available as primary");
} else {
println!("⚠️ No brokers available for failover testing");
}
Ok(())
}
#[tokio::test]
async fn test_risk_integration_with_brokers() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
// Test order rejection by risk engine
let large_order = Order {
id: format!("risk_test_{}", chrono::Utc::now().timestamp_nanos()),
symbol: config.test_symbol.clone(),
side: OrderSide::Buy,
order_type: OrderType::Market,
quantity: Quantity::new(1_000_000)?, // Intentionally large
price: None,
stop_price: None,
time_in_force: TimeInForce::ImmediateOrCancel,
created_at: std::time::SystemTime::now(),
updated_at: std::time::SystemTime::now(),
status: OrderStatus::PendingNew,
};
// Risk engine should reject this order
let risk_result = test_suite.risk_engine.validate_order(&large_order).await?;
assert!(!risk_result.is_valid, "Risk engine should reject oversized order");
// Test normal order approval
let normal_order = test_suite.create_test_order(OrderSide::Buy).await?;
let risk_result = test_suite.risk_engine.validate_order(&normal_order).await?;
assert!(risk_result.is_valid, "Risk engine should approve normal order");
println!("✅ Risk-Broker Integration: Order validation working");
Ok(())
}
#[tokio::test]
async fn test_concurrent_broker_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let test_suite = std::sync::Arc::new(tokio::sync::Mutex::new(BrokerTestSuite::new(config.clone()).await?));
// Test concurrent operations
let mut tasks = vec![];
let num_concurrent = 5;
for i in 0..num_concurrent {
let suite = test_suite.clone();
let config = config.clone();
tasks.push(tokio::spawn(async move {
let mut suite = suite.lock().await;
// Test concurrent market data requests
let start = Instant::now();
let market_data = suite.broker_manager.get_market_data(&config.test_symbol).await;
let duration = start.elapsed();
(i, market_data.is_ok(), duration)
}));
}
let results = futures::future::join_all(tasks).await;
for result in results {
let (task_id, success, duration) = result?;
assert!(success, "Concurrent operation {} failed", task_id);
assert!(
duration <= Duration::from_millis(200),
"Concurrent operation {} too slow: {}ms",
task_id, duration.as_millis()
);
}
println!("✅ Concurrent Broker Operations: {} parallel requests completed", num_concurrent);
Ok(())
}
#[tokio::test]
async fn test_broker_error_handling() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config).await?;
// Test handling of invalid symbols
let invalid_symbol = Symbol::new("INVALID_SYMBOL")?;
let result = test_suite.broker_manager.get_market_data(&invalid_symbol).await;
assert!(result.is_err(), "Should reject invalid symbol");
// Test handling of malformed orders
let invalid_order = Order {
id: "invalid".to_string(),
symbol: Symbol::new("EURUSD")?,
side: OrderSide::Buy,
order_type: OrderType::Limit,
quantity: Quantity::new(-100)?, // Invalid negative quantity
price: Some(Price::new(-1.0)?), // Invalid negative price
stop_price: None,
time_in_force: TimeInForce::GoodTillCancel,
created_at: std::time::SystemTime::now(),
updated_at: std::time::SystemTime::now(),
status: OrderStatus::PendingNew,
};
let result = test_suite.broker_manager.submit_order("any_broker", &invalid_order).await;
assert!(result.is_err(), "Should reject invalid order");
println!("✅ Broker Error Handling: Invalid inputs properly rejected");
Ok(())
}
#[tokio::test]
async fn test_comprehensive_broker_validation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = BrokerTestConfig::default();
let mut test_suite = BrokerTestSuite::new(config.clone()).await?;
let brokers = vec!["interactive_brokers", "icmarkets"];
let mut connected_brokers = 0;
let mut total_execution_time = Duration::ZERO;
let mut total_sync_time = Duration::ZERO;
for broker_name in &brokers {
let connection_status = test_suite.test_broker_connection(broker_name).await?;
if connection_status == ConnectionStatus::Connected {
connected_brokers += 1;
// Test order execution if connected
let test_order = test_suite.create_test_order(OrderSide::Buy).await?;
if let Ok(execution_result) = test_suite.test_order_execution(broker_name, &test_order).await {
total_execution_time += execution_result.execution_time;
assert!(
execution_result.execution_time <= config.max_order_latency,
"{} execution time {}ms exceeds limit",
broker_name, execution_result.execution_time.as_millis()
);
}
// Test position synchronization
if let Ok(sync_result) = test_suite.test_position_synchronization(broker_name, &config.test_symbol).await {
total_sync_time += sync_result.sync_time;
assert!(
sync_result.sync_time <= config.max_position_sync_time,
"{} sync time {}ms exceeds limit",
broker_name, sync_result.sync_time.as_millis()
);
}
// Test market data feed
if let Ok(data_latency) = test_suite.test_market_data_feed(broker_name).await {
assert!(
data_latency <= Duration::from_millis(500),
"{} market data latency {}ms too high",
broker_name, data_latency.as_millis()
);
}
println!("{}: All tests passed", broker_name);
} else {
println!("⚠️ {}: Not available for testing", broker_name);
}
}
// Overall system validation
if connected_brokers > 0 {
let avg_execution_time = total_execution_time / connected_brokers as u32;
let avg_sync_time = total_sync_time / connected_brokers as u32;
assert!(
avg_execution_time <= config.max_order_latency,
"Average execution time {}ms exceeds limit",
avg_execution_time.as_millis()
);
println!("🎯 COMPREHENSIVE BROKER VALIDATION PASSED");
println!(" Connected Brokers: {}/{}", connected_brokers, brokers.len());
println!(" Average Execution Time: {}ms", avg_execution_time.as_millis());
println!(" Average Sync Time: {}ms", avg_sync_time.as_millis());
println!(" All broker integrations meet production requirements");
} else {
println!("⚠️ No brokers available for comprehensive testing");
}
Ok(())
}