//! Integration tests for universe selection module use trading_agent_service::universe::{AssetClass, Region, UniverseCriteria, UniverseSelector}; #[tokio::test] async fn test_select_universe_with_default_criteria() { // Setup database connection let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); // Run migrations sqlx::migrate!("../../migrations") .run(&pool) .await .expect("Failed to run migrations"); let selector = UniverseSelector::new(pool); // Test with default criteria let criteria = UniverseCriteria::default(); let result = selector.select_universe(criteria).await; assert!( result.is_ok(), "Universe selection should succeed with default criteria" ); let universe = result.expect("Universe should be present"); // Verify universe properties assert!(!universe.universe_id.is_empty()); assert!(!universe.instruments.is_empty(), "Should have instruments"); assert_eq!( universe.metrics.total_instruments, universe.instruments.len() ); assert!(universe.metrics.avg_liquidity_score > 0.0); assert!(universe.metrics.avg_volatility > 0.0); } #[tokio::test] async fn test_select_universe_with_high_liquidity() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with high liquidity requirement let criteria = UniverseCriteria { min_liquidity: 0.90, // Only ES.FUT, NQ.FUT, CL.FUT qualify ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // All instruments should have liquidity >= 0.90 for instrument in &universe.instruments { assert!( instrument.liquidity_score >= 0.90, "Instrument {} has liquidity {} which is below threshold", instrument.symbol, instrument.liquidity_score ); } } #[tokio::test] async fn test_select_universe_with_low_volatility() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with low volatility requirement let criteria = UniverseCriteria { max_volatility: 0.20, // Only ES.FUT, ZN.FUT, 6E.FUT qualify ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // All instruments should have volatility <= 0.20 for instrument in &universe.instruments { assert!( instrument.volatility <= 0.20, "Instrument {} has volatility {} which exceeds threshold", instrument.symbol, instrument.volatility ); } } #[tokio::test] async fn test_select_universe_by_asset_class() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with currencies only let criteria = UniverseCriteria { asset_classes: vec![AssetClass::Currencies], regions: vec![Region::Global], // 6E.FUT is in Global region ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // Should only include 6E.FUT assert_eq!(universe.instruments.len(), 1); assert_eq!(universe.instruments[0].symbol.as_str(), "6E.FUT"); } #[tokio::test] async fn test_select_universe_by_region() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with Global region only let criteria = UniverseCriteria { regions: vec![Region::Global], asset_classes: vec![ AssetClass::Futures, AssetClass::Currencies, AssetClass::Commodities, ], ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // Should only include instruments from Global region for instrument in &universe.instruments { assert_eq!(instrument.region, Region::Global); } } #[tokio::test] async fn test_get_universe_by_id() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Create universe let criteria = UniverseCriteria::default(); let universe = selector .select_universe(criteria) .await .expect("Failed to create universe"); // Retrieve universe by ID let retrieved = selector.get_universe(&universe.universe_id).await; assert!(retrieved.is_ok()); let retrieved_universe = retrieved.expect("Universe should be retrieved"); assert_eq!(retrieved_universe.universe_id, universe.universe_id); assert_eq!( retrieved_universe.instruments.len(), universe.instruments.len() ); } #[tokio::test] async fn test_get_nonexistent_universe() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Try to get non-existent universe let result = selector.get_universe("nonexistent_id").await; assert!(result.is_err()); } #[tokio::test] async fn test_update_criteria() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Create universe let criteria = UniverseCriteria::default(); let universe = selector .select_universe(criteria) .await .expect("Failed to create universe"); // Update criteria let new_criteria = UniverseCriteria { min_liquidity: 0.95, // Very high threshold ..UniverseCriteria::default() }; let result = selector .update_criteria(&universe.universe_id, new_criteria) .await; assert!(result.is_ok()); let updated_universe = result.expect("Update should succeed"); // New universe should have different ID (new universe created) assert_ne!(updated_universe.universe_id, universe.universe_id); // New universe should have fewer instruments (higher threshold) assert!(updated_universe.instruments.len() <= universe.instruments.len()); } #[tokio::test] async fn test_universe_performance() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let start = std::time::Instant::now(); let criteria = UniverseCriteria::default(); let _universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); let duration = start.elapsed(); // Performance target: < 1 second assert!( duration.as_millis() < 1000, "Universe selection took {}ms (target: <1000ms)", duration.as_millis() ); println!("Universe selection completed in {}ms", duration.as_millis()); } #[tokio::test] async fn test_invalid_criteria_min_liquidity() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria { min_liquidity: 1.5, // Invalid (> 1.0) ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_err()); } #[tokio::test] async fn test_invalid_criteria_max_volatility() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria { max_volatility: -0.1, // Invalid (< 0.0) ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_err()); } #[tokio::test] async fn test_no_instruments_match() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Set impossible criteria let criteria = UniverseCriteria { min_liquidity: 0.99, // Very high max_volatility: 0.01, // Very low — no instrument can satisfy both ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_err()); } // ============================================================================ // EDGE CASE TESTS // ============================================================================ #[tokio::test] async fn test_extreme_liquidity_threshold() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with extremely high liquidity threshold let criteria = UniverseCriteria { min_liquidity: 0.99, // Only instruments with 99%+ liquidity ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; // Should fail because no instruments meet this threshold assert!( result.is_err(), "Should fail with extreme liquidity threshold" ); } #[tokio::test] async fn test_minimal_liquidity_threshold() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with minimal liquidity threshold let criteria = UniverseCriteria { min_liquidity: 0.0, // Accept all instruments ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // Should include all instruments that pass other filters assert!( !universe.instruments.is_empty(), "Should have at least one instrument" ); } #[tokio::test] async fn test_single_symbol_universe() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Create criteria that only matches ES.FUT let criteria = UniverseCriteria { min_liquidity: 0.95, // Only ES.FUT has 0.95 max_volatility: 0.20, // ES.FUT has 0.20 asset_classes: vec![AssetClass::Futures], regions: vec![Region::NorthAmerica], ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // Should only have ES.FUT assert_eq!(universe.instruments.len(), 1); assert_eq!(universe.instruments[0].symbol.as_str(), "ES.FUT"); } #[tokio::test] async fn test_multiple_asset_classes() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with multiple asset classes let criteria = UniverseCriteria { asset_classes: vec![ AssetClass::Futures, AssetClass::Currencies, AssetClass::Commodities, ], regions: vec![Region::NorthAmerica, Region::Global], ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // Should have instruments from multiple asset classes let mut asset_class_names = std::collections::HashSet::new(); for instrument in &universe.instruments { asset_class_names.insert(format!("{:?}", instrument.asset_class)); } assert!( asset_class_names.len() >= 2, "Should have instruments from multiple asset classes" ); } #[tokio::test] async fn test_market_cap_filtering() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Test with high market cap threshold let criteria = UniverseCriteria { min_market_cap: Some(8_000_000_000.0), // $8B - only ES.FUT and NQ.FUT ..UniverseCriteria::default() }; let result = selector.select_universe(criteria).await; assert!(result.is_ok()); let universe = result.expect("Universe should be present"); // All instruments should have market cap >= $8B for instrument in &universe.instruments { if let Some(cap) = instrument.market_cap { assert!( cap >= 8_000_000_000.0, "Instrument {} has market cap ${} which is below threshold", instrument.symbol, cap ); } } // Should only have ES.FUT and NQ.FUT assert!(universe.instruments.len() <= 2); } // ============================================================================ // DETERMINISM AND REPRODUCIBILITY TESTS // ============================================================================ #[tokio::test] async fn test_deterministic_results() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria::default(); // Run selection twice with same criteria let universe1 = selector .select_universe(criteria.clone()) .await .expect("First selection failed"); let universe2 = selector .select_universe(criteria.clone()) .await .expect("Second selection failed"); // Results should be deterministic (same number of instruments) assert_eq!( universe1.instruments.len(), universe2.instruments.len(), "Results should be deterministic" ); // Same symbols should be selected (in any order) let symbols1: std::collections::HashSet<_> = universe1 .instruments .iter() .map(|i| i.symbol.as_str()) .collect(); let symbols2: std::collections::HashSet<_> = universe2 .instruments .iter() .map(|i| i.symbol.as_str()) .collect(); assert_eq!(symbols1, symbols2, "Same symbols should be selected"); } #[tokio::test] async fn test_reproducible_metrics() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria::default(); // Run selection twice let universe1 = selector .select_universe(criteria.clone()) .await .expect("First selection failed"); let universe2 = selector .select_universe(criteria.clone()) .await .expect("Second selection failed"); // Metrics should be identical assert_eq!( universe1.metrics.total_instruments, universe2.metrics.total_instruments ); assert!( (universe1.metrics.avg_liquidity_score - universe2.metrics.avg_liquidity_score).abs() < 1e-10 ); assert!((universe1.metrics.avg_volatility - universe2.metrics.avg_volatility).abs() < 1e-10); assert!((universe1.metrics.avg_spread_bps - universe2.metrics.avg_spread_bps).abs() < 1e-10); } // ============================================================================ // PERFORMANCE TESTS // ============================================================================ #[tokio::test] async fn test_performance_with_multiple_filters() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let start = std::time::Instant::now(); // Test with complex criteria let criteria = UniverseCriteria { min_liquidity: 0.85, max_volatility: 0.30, asset_classes: vec![ AssetClass::Futures, AssetClass::Currencies, AssetClass::Commodities, ], regions: vec![Region::NorthAmerica, Region::Global], min_market_cap: Some(4_000_000_000.0), ..UniverseCriteria::default() }; let _universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); let duration = start.elapsed(); // Performance target: < 1 second assert!( duration.as_millis() < 1000, "Universe selection with multiple filters took {}ms (target: <1000ms)", duration.as_millis() ); println!( "Complex universe selection completed in {}ms", duration.as_millis() ); } #[tokio::test] async fn test_performance_sequential_selections() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let start = std::time::Instant::now(); // Run 10 sequential selections for _ in 0..10 { let criteria = UniverseCriteria::default(); let _universe = selector .select_universe(criteria) .await .expect("Selection failed"); } let duration = start.elapsed(); let avg_duration = duration.as_millis() / 10; // Average should be well under 1 second assert!( avg_duration < 1000, "Average universe selection took {}ms (target: <1000ms)", avg_duration ); println!( "10 sequential selections completed in {}ms (avg: {}ms)", duration.as_millis(), avg_duration ); } // ============================================================================ // METRICS VALIDATION TESTS // ============================================================================ #[tokio::test] async fn test_metrics_accuracy() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria::default(); let universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); // Verify metrics are calculated correctly assert_eq!( universe.metrics.total_instruments, universe.instruments.len() ); // Calculate expected averages let expected_avg_liquidity = universe .instruments .iter() .map(|i| i.liquidity_score) .sum::() / universe.instruments.len() as f64; let expected_avg_volatility = universe .instruments .iter() .map(|i| i.volatility) .sum::() / universe.instruments.len() as f64; let expected_avg_spread = universe .instruments .iter() .map(|i| i.spread_bps) .sum::() / universe.instruments.len() as f64; // Verify metrics match calculations assert!((universe.metrics.avg_liquidity_score - expected_avg_liquidity).abs() < 1e-10); assert!((universe.metrics.avg_volatility - expected_avg_volatility).abs() < 1e-10); assert!((universe.metrics.avg_spread_bps - expected_avg_spread).abs() < 1e-10); } #[tokio::test] async fn test_asset_class_distribution() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria { asset_classes: vec![AssetClass::Futures, AssetClass::Currencies], regions: vec![Region::NorthAmerica, Region::Global], ..UniverseCriteria::default() }; let universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); // Verify asset class distribution matches actual instruments let mut expected_distribution = std::collections::HashMap::new(); for instrument in &universe.instruments { let key = format!("{:?}", instrument.asset_class); *expected_distribution.entry(key).or_insert(0) += 1; } assert_eq!( universe.metrics.asset_class_distribution, expected_distribution ); } // ============================================================================ // REAL SYMBOL VALIDATION TESTS // ============================================================================ #[tokio::test] async fn test_real_symbols_es_nq() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria { min_liquidity: 0.90, // ES.FUT (0.95), NQ.FUT (0.92), CL.FUT (0.90) ..UniverseCriteria::default() }; let universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); // Should include ES.FUT and NQ.FUT let symbols: std::collections::HashSet<_> = universe .instruments .iter() .map(|i| i.symbol.as_str()) .collect(); assert!(symbols.contains("ES.FUT"), "Should include ES.FUT"); assert!(symbols.contains("NQ.FUT"), "Should include NQ.FUT"); } #[tokio::test] async fn test_real_symbols_all_available() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); // Very permissive criteria to get all symbols let criteria = UniverseCriteria { min_liquidity: 0.0, max_volatility: 1.0, asset_classes: vec![ AssetClass::Futures, AssetClass::Currencies, AssetClass::Commodities, ], regions: vec![ Region::NorthAmerica, Region::Global, Region::Europe, Region::Asia, ], min_market_cap: None, ..UniverseCriteria::default() }; let universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); // Should have all 5 instruments: ES.FUT, NQ.FUT, ZN.FUT, 6E.FUT, CL.FUT let symbols: std::collections::HashSet<_> = universe .instruments .iter() .map(|i| i.symbol.as_str()) .collect(); assert_eq!(symbols.len(), 5, "Should have all 5 instruments"); assert!(symbols.contains("ES.FUT")); assert!(symbols.contains("NQ.FUT")); assert!(symbols.contains("ZN.FUT")); assert!(symbols.contains("6E.FUT")); assert!(symbols.contains("CL.FUT")); } #[tokio::test] async fn test_real_symbol_properties() { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = sqlx::PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let selector = UniverseSelector::new(pool); let criteria = UniverseCriteria::default(); let universe = selector .select_universe(criteria) .await .expect("Failed to select universe"); // Verify properties of real symbols for instrument in &universe.instruments { // All should have valid properties assert!(instrument.liquidity_score > 0.0 && instrument.liquidity_score <= 1.0); assert!(instrument.volatility > 0.0 && instrument.volatility <= 1.0); assert!(instrument.avg_daily_volume > 0.0); assert!(instrument.spread_bps > 0.0); assert_eq!(instrument.exchange, "CME"); } }