use chrono::{DateTime, Utc}; use common::{OrderSide, OrderType, Price, Quantity, Symbol, TimeInForce}; use rand::{thread_rng, Rng}; use std::collections::HashMap; use uuid::Uuid; /// Test market data event structure /// /// Note: This is a test-specific type, not the proto MarketDataEvent #[derive(Debug, Clone)] pub struct MarketDataEvent { pub id: Uuid, pub symbol: Symbol, pub timestamp: DateTime, pub bid: Price, pub ask: Price, pub bid_size: Quantity, pub ask_size: Quantity, pub last_price: Option, pub volume: Option, } /// Test utilities for generating market data and orders pub struct TestDataGenerator { symbols: Vec, prices: HashMap, } impl Default for TestDataGenerator { fn default() -> Self { Self::new() } } impl TestDataGenerator { pub fn new() -> Self { let symbols = vec![ Symbol::new("EURUSD".to_string()), Symbol::new("GBPUSD".to_string()), Symbol::new("USDJPY".to_string()), Symbol::new("USDCHF".to_string()), Symbol::new("AUDUSD".to_string()), ]; let mut prices = HashMap::new(); prices.insert( Symbol::new("EURUSD".to_string()), Price::new(1.0520).unwrap(), ); // 1.0520 prices.insert( Symbol::new("GBPUSD".to_string()), Price::new(1.2845).unwrap(), ); // 1.2845 prices.insert( Symbol::new("USDJPY".to_string()), Price::new(148.55).unwrap(), ); // 148.55 prices.insert( Symbol::new("USDCHF".to_string()), Price::new(0.8750).unwrap(), ); // 0.8750 prices.insert( Symbol::new("AUDUSD".to_string()), Price::new(0.6750).unwrap(), ); // 0.6750 Self { symbols, prices } } pub fn generate_market_data(&mut self, symbol: &Symbol) -> MarketDataEvent { let mut rng = thread_rng(); let current_price = *self.prices.get(symbol).unwrap(); // Generate small random price movement let change_pct = rng.gen_range(-0.001..0.001); // ±0.1% let change = current_price.to_f64() * change_pct; let new_price = Price::new(current_price.to_f64() + change).unwrap(); self.prices.insert(symbol.clone(), new_price); MarketDataEvent { id: Uuid::new_v4(), symbol: symbol.clone(), timestamp: Utc::now(), bid: Price::new(new_price.to_f64() - 0.0002).unwrap(), // 2 pip spread ask: new_price, bid_size: Quantity::new(rng.gen_range(100000..1000000) as f64).unwrap(), ask_size: Quantity::new(rng.gen_range(100000..1000000) as f64).unwrap(), last_price: Some(new_price), volume: Some(Quantity::new(rng.gen_range(50000..500000) as f64).unwrap()), } } pub fn generate_order_request(&self, symbol: &Symbol) -> OrderRequest { let mut rng = thread_rng(); let current_price = self.prices.get(symbol).unwrap(); OrderRequest { id: Uuid::new_v4(), symbol: symbol.clone(), side: if rng.gen_bool(0.5) { OrderSide::Buy } else { OrderSide::Sell }, quantity: Quantity::new(rng.gen_range(10000..100000) as f64).unwrap(), // 10K to 100K units order_type: OrderType::Market, price: Some(*current_price), time_in_force: TimeInForce::ImmediateOrCancel, timestamp: Utc::now(), } } pub fn get_random_symbol(&self) -> &Symbol { let mut rng = thread_rng(); &self.symbols[rng.gen_range(0..self.symbols.len())] } pub fn get_all_symbols(&self) -> &[Symbol] { &self.symbols } } #[derive(Debug, Clone)] pub struct OrderRequest { pub id: Uuid, pub symbol: Symbol, pub side: OrderSide, pub quantity: Quantity, pub order_type: OrderType, pub price: Option, pub time_in_force: TimeInForce, pub timestamp: DateTime, } /// Trading event for database persistence testing #[derive(Debug, Clone)] pub struct TradingEvent { pub id: Uuid, pub event_type: String, pub symbol: String, pub timestamp: DateTime, pub data: serde_json::Value, } /// Test assertion helpers pub mod assertions { use super::*; use std::time::Duration; pub fn assert_within_tolerance(actual: f64, expected: f64, tolerance_pct: f64) { let tolerance = expected * tolerance_pct; assert!( (actual - expected).abs() <= tolerance, "Value {} is not within {}% tolerance of expected {}", actual, expected, tolerance_pct * 100.0 ); } pub fn assert_latency_under(duration: Duration, max_latency: Duration) { assert!( duration <= max_latency, "Latency {:?} exceeds maximum allowed {:?}", duration, max_latency ); } pub fn assert_price_reasonable(price: &Price, symbol: &Symbol) { let value = price.to_f64(); match symbol.as_str() { "EURUSD" | "GBPUSD" | "AUDUSD" => { assert!( value > 0.5 && value < 2.0, "Price {} unreasonable for {}", value, symbol ); }, "USDJPY" => { assert!( value > 100.0 && value < 200.0, "Price {} unreasonable for {}", value, symbol ); }, "USDCHF" => { assert!( value > 0.7 && value < 1.2, "Price {} unreasonable for {}", value, symbol ); }, _ => {}, // Skip validation for unknown symbols } } } /// Performance profiling utilities for E2E tests pub struct PerformanceProfiler { start_time: std::time::Instant, checkpoints: Vec<(String, std::time::Duration)>, } impl Default for PerformanceProfiler { fn default() -> Self { Self::new() } } impl PerformanceProfiler { pub fn new() -> Self { Self { start_time: std::time::Instant::now(), checkpoints: Vec::new(), } } pub fn checkpoint(&mut self, name: &str) { let elapsed = self.start_time.elapsed(); self.checkpoints.push((name.to_string(), elapsed)); } pub fn print_summary(&self) { println!("\n=== Performance Profile ==="); let mut last_duration = std::time::Duration::ZERO; for (name, duration) in &self.checkpoints { let delta = *duration - last_duration; println!("{}: {:?} (delta: {:?})", name, duration, delta); last_duration = *duration; } println!("Total: {:?}\n", self.start_time.elapsed()); } } /// Test utility functions pub struct TestUtils; impl TestUtils { /// Setup test logging pub fn setup_test_logging() { let _ = tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .try_init(); } /// Wait for a condition to be true with timeout pub async fn wait_for_condition( condition: F, timeout_secs: u64, check_interval_ms: u64, ) -> anyhow::Result<()> where F: Fn() -> Fut, Fut: std::future::Future, { use tokio::time::{sleep, Duration, Instant}; let start = Instant::now(); let timeout = Duration::from_secs(timeout_secs); let interval = Duration::from_millis(check_interval_ms); while start.elapsed() < timeout { if condition().await { return Ok(()); } sleep(interval).await; } Err(anyhow::anyhow!( "Condition not met within {} seconds", timeout_secs )) } /// Check if a service is healthy via HTTP pub async fn check_service_health(endpoint: &str) -> anyhow::Result { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build()?; match client.get(endpoint).send().await { Ok(response) => Ok(response.status().is_success()), Err(_) => Ok(false), } } } /// Stub types for E2E testing - These are lightweight test doubles /// for performance-critical components that are tested separately /// Trading operations tracker for latency testing pub struct TradingOperations { operations: std::sync::Arc>>, } impl Default for TradingOperations { fn default() -> Self { Self::new() } } impl TradingOperations { pub fn new() -> Self { Self { operations: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), } } pub fn record_order_submission(&self, order_id: String, symbol: String) { let mut ops = self.operations.lock().unwrap(); ops.push((order_id, symbol, std::time::Instant::now())); } } /// SIMD price operations stub for testing (actual implementation in trading_engine) pub struct SimdPriceOps; impl SimdPriceOps { pub fn new() -> anyhow::Result { Ok(Self) } pub fn vectorized_mean(&self, prices: &[f64]) -> anyhow::Result { if prices.is_empty() { return Err(anyhow::anyhow!("Empty price array")); } Ok(prices.iter().sum::() / prices.len() as f64) } } /// Lock-free ring buffer stub for testing pub struct LockFreeRingBuffer { buffer: std::sync::Arc>>, capacity: usize, } impl LockFreeRingBuffer { pub fn new(capacity: usize) -> Self { Self { buffer: std::sync::Arc::new(std::sync::Mutex::new(Vec::with_capacity(capacity))), capacity, } } pub fn try_push(&self, item: T) -> bool { let mut buffer = self.buffer.lock().unwrap(); if buffer.len() < self.capacity { buffer.push(item); true } else { false } } } /// Small batch processor for testing pub struct SmallBatchProcessor; impl Default for SmallBatchProcessor { fn default() -> Self { Self::new() } } impl SmallBatchProcessor { pub fn new() -> Self { Self } pub fn process_batch(&self, orders: Vec) -> anyhow::Result { // Simulate minimal processing - generic to accept any order type Ok(orders.len()) } } /// Environment setup utilities pub mod env { use std::env; pub fn setup_test_environment() { // Set up test-specific environment variables env::set_var("RUST_LOG", "debug"); env::set_var("DATABASE_URL", get_test_database_url()); env::set_var("TRADING_SERVICE_PORT", "50051"); env::set_var("BACKTESTING_SERVICE_PORT", "50052"); env::set_var("CONFIG_SERVICE_PORT", "50053"); } pub fn get_test_database_url() -> String { env::var("TEST_DATABASE_URL") .unwrap_or_else(|_| "postgresql://localhost/foxhunt_test".to_string()) } pub fn is_ci_environment() -> bool { env::var("CI").is_ok() || env::var("GITHUB_ACTIONS").is_ok() } } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn test_data_generator_creates_valid_market_data() { let mut generator = TestDataGenerator::new(); let symbol = Symbol::new("EURUSD".to_string()); let market_data = generator.generate_market_data(&symbol); assert_eq!(market_data.symbol, symbol); assert!(market_data.bid < market_data.ask); assertions::assert_price_reasonable(&market_data.bid, &symbol); assertions::assert_price_reasonable(&market_data.ask, &symbol); } #[test] fn test_order_request_generation() { let generator = TestDataGenerator::new(); let symbol = Symbol::new("GBPUSD".to_string()); let order = generator.generate_order_request(&symbol); assert_eq!(order.symbol, symbol); assert!(order.quantity.to_f64() > 0.0); if let Some(price) = &order.price { assertions::assert_price_reasonable(price, &symbol); } } #[test] fn test_assertion_helpers() { assertions::assert_within_tolerance(100.0, 99.0, 0.02); // 2% tolerance assertions::assert_latency_under(Duration::from_millis(5), Duration::from_millis(10)); } }