use anyhow::Result; use chrono::{DateTime, Utc}; use trading_engine::types::{ basic::{Price, Quantity, Symbol}, events::MarketDataEvent, financial::{OrderSide, OrderType, TimeInForce}, }; use rand::{thread_rng, Rng}; use trading_engine::types::prelude::Decimal; use std::collections::HashMap; use uuid::Uuid; /// Test utilities for generating market data and orders pub struct TestDataGenerator { symbols: Vec, prices: HashMap, } impl TestDataGenerator { pub fn new() -> Self { let symbols = vec![ Symbol::new("EURUSD"), Symbol::new("GBPUSD"), Symbol::new("USDJPY"), Symbol::new("USDCHF"), Symbol::new("AUDUSD"), ]; let mut prices = HashMap::new(); prices.insert(Symbol::new("EURUSD"), Price::new(Decimal::new(10520, 4))); // 1.0520 prices.insert(Symbol::new("GBPUSD"), Price::new(Decimal::new(12845, 4))); // 1.2845 prices.insert(Symbol::new("USDJPY"), Price::new(Decimal::new(1485500, 2))); // 148.55 prices.insert(Symbol::new("USDCHF"), Price::new(Decimal::new(8750, 4))); // 0.8750 prices.insert(Symbol::new("AUDUSD"), Price::new(Decimal::new(6750, 4))); // 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().clone(); // Generate small random price movement let change_pct = rng.gen_range(-0.001..0.001); // ±0.1% let change = current_price.value() * Decimal::from_f64(change_pct).unwrap(); let new_price = Price::new(current_price.value() + change); self.prices.insert(symbol.clone(), new_price.clone()); MarketDataEvent { id: Uuid::new_v4(), symbol: symbol.clone(), timestamp: Utc::now(), bid: Price::new(new_price.value() - Decimal::new(2, 4)), // 2 pip spread ask: new_price, bid_size: Quantity::new(rng.gen_range(100000..1000000)), ask_size: Quantity::new(rng.gen_range(100000..1000000)), last_price: Some(new_price), volume: Some(Quantity::new(rng.gen_range(50000..500000))), } } 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)), // 10K to 100K units order_type: OrderType::Market, price: Some(current_price.clone()), time_in_force: TimeInForce::IOC, 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, } /// 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.value().to_f64().unwrap(); 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 } } } /// 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::*; #[test] fn test_data_generator_creates_valid_market_data() { let mut generator = TestDataGenerator::new(); let symbol = Symbol::new("EURUSD"); 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"); let order = generator.generate_order_request(&symbol); assert_eq!(order.symbol, symbol); assert!(order.quantity.value() > 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) ); } }