//! Integration tests for database-backed configuration //! //! These tests verify that the DatabaseConfigLoader correctly loads //! configurations from PostgreSQL and that the migration seed data //! creates valid configurations. //! //! ## Prerequisites //! - PostgreSQL database running //! - Migrations 015 and 016 applied //! - Database connection string in DATABASE_URL environment variable //! //! ## Test Coverage //! - Load production, development, and aggressive strategies //! - Validate configuration parameters //! - Test hot-reload notifications #![allow(unused_crate_dependencies)] //! - Verify model and feature associations #![cfg(feature = "postgres")] use adaptive_strategy::config_types::{ AdaptiveStrategyConfig, ExecutionAlgorithm, PositionSizingMethod, RegimeDetectionMethod, }; use adaptive_strategy::database_loader::DatabaseConfigLoader; use std::env; /// Helper to get database URL from environment fn get_database_url() -> String { env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://postgres:postgres@localhost:5432/foxhunt_test".to_string() }) } /// Helper to create a DatabaseConfigLoader for testing async fn create_loader() -> DatabaseConfigLoader { let url = get_database_url(); DatabaseConfigLoader::new(&url) .await .expect("Failed to create DatabaseConfigLoader") } // ============================================================================ // CONFIGURATION LOADING TESTS // ============================================================================ #[tokio::test] async fn test_load_production_config() { let loader = create_loader().await; let config = loader .load_config("default-production") .await .expect("Failed to load config") .expect("Production config not found"); // Verify basic metadata assert_eq!(config.strategy_id, "default-production"); assert_eq!(config.name, "Production Default Strategy"); // Verify conservative production values assert_eq!(config.general.execution_interval.as_millis(), 100); assert_eq!(config.general.max_concurrent_operations, 10); // Verify conservative risk settings assert_eq!(config.risk.max_position_size, 0.05); // 5% assert_eq!(config.risk.max_leverage, 1.5); assert_eq!(config.risk.kelly_fraction, 0.25); assert_eq!( config.risk.position_sizing_method, PositionSizingMethod::Kelly ); // Verify ensemble settings assert_eq!(config.ensemble.max_parallel_models, 4); assert_eq!(config.ensemble.min_model_weight, 0.01); assert_eq!(config.ensemble.max_model_weight, 0.50); // Verify execution settings assert_eq!(config.execution.algorithm, ExecutionAlgorithm::TWAP); assert_eq!(config.execution.max_order_size, 10000.0); assert!(config.execution.smart_routing_enabled); // Verify regime detection assert_eq!( config.regime.detection_method, RegimeDetectionMethod::HMM ); assert_eq!(config.regime.lookback_window, 252); // Verify models loaded assert!(!config.models.is_empty(), "No models loaded"); assert_eq!(config.models.len(), 3); // Production has 3 models // Verify model weights sum to approximately 1.0 let total_weight: f64 = config.models.iter().map(|m| m.initial_weight).sum(); assert!( (total_weight - 1.0).abs() < 0.01, "Model weights sum to {} (expected 1.0)", total_weight ); // Verify features loaded assert!(!config.features.is_empty(), "No features loaded"); } #[tokio::test] async fn test_load_development_config() { let loader = create_loader().await; let config = loader .load_config("development") .await .expect("Failed to load config") .expect("Development config not found"); assert_eq!(config.strategy_id, "development"); assert_eq!(config.name, "Development Strategy"); // Verify permissive development settings assert_eq!(config.general.execution_interval.as_millis(), 50); // Faster assert_eq!(config.general.max_concurrent_operations, 20); // Higher concurrency // Verify higher risk limits assert_eq!(config.risk.max_position_size, 0.20); // 20% (higher) assert_eq!(config.risk.max_leverage, 3.0); // Higher leverage assert_eq!(config.risk.kelly_fraction, 0.50); // More aggressive // Verify more models assert_eq!(config.ensemble.max_parallel_models, 8); assert_eq!(config.models.len(), 5); // Development has 5 models // Verify execution algorithm assert_eq!(config.execution.algorithm, ExecutionAlgorithm::VWAP); // Verify regime detection method assert_eq!( config.regime.detection_method, RegimeDetectionMethod::Threshold ); } #[tokio::test] async fn test_load_aggressive_config() { let loader = create_loader().await; let config = loader .load_config("aggressive") .await .expect("Failed to load config") .expect("Aggressive config not found"); assert_eq!(config.strategy_id, "aggressive"); assert_eq!(config.name, "Aggressive High-Frequency Strategy"); // Verify HFT settings assert_eq!(config.general.execution_interval.as_millis(), 10); // 10ms HFT assert_eq!(config.general.max_concurrent_operations, 50); // Very high concurrency // Verify aggressive risk settings assert_eq!(config.risk.max_position_size, 0.15); // 15% assert_eq!(config.risk.kelly_fraction, 0.40); assert_eq!(config.risk.position_sizing_method, PositionSizingMethod::PPO); // Verify minimal ensemble for speed assert_eq!(config.ensemble.max_parallel_models, 2); // Only 2 for latency assert_eq!(config.models.len(), 2); // Aggressive has 2 models // Verify execution algorithm assert_eq!(config.execution.algorithm, ExecutionAlgorithm::IS); assert_eq!(config.execution.max_slippage_bps, 5.0); // Tight slippage // Verify regime detection assert_eq!( config.regime.detection_method, RegimeDetectionMethod::MLClassifier ); assert_eq!(config.regime.lookback_window, 50); // Short window for HFT } #[tokio::test] async fn test_nonexistent_config() { let loader = create_loader().await; let result = loader .load_config("nonexistent-strategy") .await .expect("Should not error"); assert!( result.is_none(), "Should return None for nonexistent strategy" ); } // ============================================================================ // VALIDATION TESTS // ============================================================================ #[tokio::test] async fn test_production_config_validation() { let loader = create_loader().await; let config = loader .load_config("default-production") .await .expect("Failed to load") .expect("Config not found"); // Validate should succeed for production config config .validate() .expect("Production config should be valid"); } #[tokio::test] async fn test_development_config_validation() { let loader = create_loader().await; let config = loader .load_config("development") .await .expect("Failed to load") .expect("Config not found"); config .validate() .expect("Development config should be valid"); } #[tokio::test] async fn test_aggressive_config_validation() { let loader = create_loader().await; let config = loader .load_config("aggressive") .await .expect("Failed to load") .expect("Config not found"); config .validate() .expect("Aggressive config should be valid"); } // ============================================================================ // MODEL CONFIGURATION TESTS // ============================================================================ #[tokio::test] async fn test_production_models() { let loader = create_loader().await; let config = loader .load_config("default-production") .await .expect("Failed to load") .expect("Config not found"); // Verify expected models let model_types: Vec = config .models .iter() .map(|m| m.model_type.clone()) .collect(); assert!(model_types.contains(&"mamba2".to_string())); assert!(model_types.contains(&"tlob".to_string())); assert!(model_types.contains(&"lstm".to_string())); // Verify all models are enabled for model in &config.models { assert!(model.enabled, "Model {} should be enabled", model.name); } // Verify model parameters are valid JSON for model in &config.models { assert!( model.parameters.is_object(), "Model {} should have object parameters", model.name ); } } #[tokio::test] async fn test_development_models() { let loader = create_loader().await; let config = loader .load_config("development") .await .expect("Failed to load") .expect("Config not found"); // Development should have more models assert_eq!(config.models.len(), 5); let model_types: Vec = config .models .iter() .map(|m| m.model_type.clone()) .collect(); // Verify comprehensive model set assert!(model_types.contains(&"mamba2".to_string())); assert!(model_types.contains(&"tlob".to_string())); assert!(model_types.contains(&"dqn".to_string())); assert!(model_types.contains(&"ppo".to_string())); assert!(model_types.contains(&"liquid".to_string())); } #[tokio::test] async fn test_aggressive_models() { let loader = create_loader().await; let config = loader .load_config("aggressive") .await .expect("Failed to load") .expect("Config not found"); // Aggressive should have minimal models for speed assert_eq!(config.models.len(), 2); // Verify HFT-optimized models let model_types: Vec = config .models .iter() .map(|m| m.model_type.clone()) .collect(); assert!(model_types.contains(&"tlob".to_string())); assert!(model_types.contains(&"ppo".to_string())); // Verify weights favor TLOB (60/40 split) let tlob_model = config .models .iter() .find(|m| m.model_type == "tlob") .expect("TLOB model not found"); assert_eq!(tlob_model.initial_weight, 0.60); } // ============================================================================ // FEATURE CONFIGURATION TESTS // ============================================================================ #[tokio::test] async fn test_production_features() { let loader = create_loader().await; let config = loader .load_config("default-production") .await .expect("Failed to load") .expect("Config not found"); // Verify core features present let feature_names: Vec = config.features.iter().map(|f| f.name.clone()).collect(); assert!(feature_names.contains(&"vpin".to_string())); assert!(feature_names.contains(&"order_flow".to_string())); assert!(feature_names.contains(&"bid_ask_spread".to_string())); assert!(feature_names.contains(&"volatility".to_string())); // Verify required features let required_features: Vec<&str> = config .features .iter() .filter(|f| f.required) .map(|f| f.name.as_str()) .collect(); assert!( !required_features.is_empty(), "Should have at least one required feature" ); assert!(required_features.contains(&"vpin")); } #[tokio::test] async fn test_development_features() { let loader = create_loader().await; let config = loader .load_config("development") .await .expect("Failed to load") .expect("Config not found"); // Development should have more features assert!( config.features.len() >= 6, "Development should have at least 6 features" ); let feature_names: Vec = config.features.iter().map(|f| f.name.clone()).collect(); // Verify extended feature set assert!(feature_names.contains(&"vpin".to_string())); assert!(feature_names.contains(&"depth_imbalance".to_string())); assert!(feature_names.contains(&"rsi".to_string())); assert!(feature_names.contains(&"macd".to_string())); } #[tokio::test] async fn test_aggressive_features() { let loader = create_loader().await; let config = loader .load_config("aggressive") .await .expect("Failed to load") .expect("Config not found"); // Aggressive should have minimal features for speed assert_eq!(config.features.len(), 3); let feature_names: Vec = config.features.iter().map(|f| f.name.clone()).collect(); // Verify minimal feature set assert!(feature_names.contains(&"vpin".to_string())); assert!(feature_names.contains(&"bid_ask_spread".to_string())); assert!(feature_names.contains(&"volatility".to_string())); } // ============================================================================ // HOT-RELOAD TESTS // ============================================================================ #[tokio::test] async fn test_hot_reload_notification() { // This test would verify PostgreSQL NOTIFY/LISTEN hot-reload // Ignored by default as it requires full database setup let loader = create_loader().await; // TODO: Implement hot-reload test // 1. Listen for notifications // 2. Update configuration in database // 3. Verify notification received // 4. Reload configuration // 5. Verify new values loaded let _config = loader .load_config("default-production") .await .expect("Failed to load"); // Placeholder for actual hot-reload test } // ============================================================================ // COMPARISON TESTS // ============================================================================ #[tokio::test] async fn test_strategy_comparison() { let loader = create_loader().await; let production = loader .load_config("default-production") .await .expect("Failed to load") .expect("Production not found"); let development = loader .load_config("development") .await .expect("Failed to load") .expect("Development not found"); let aggressive = loader .load_config("aggressive") .await .expect("Failed to load") .expect("Aggressive not found"); // Verify execution intervals increase in speed: production > development > aggressive assert!( production.general.execution_interval > development.general.execution_interval, "Production should be slower than development" ); assert!( development.general.execution_interval > aggressive.general.execution_interval, "Development should be slower than aggressive" ); // Verify risk increases: production < development assert!( production.risk.max_position_size < development.risk.max_position_size, "Production should have lower position size than development" ); // Verify model count decreases for speed: development > production > aggressive assert!( development.models.len() > production.models.len(), "Development should have more models than production" ); assert!( production.models.len() > aggressive.models.len(), "Production should have more models than aggressive" ); } // ============================================================================ // ERROR HANDLING TESTS // ============================================================================ #[tokio::test] async fn test_invalid_database_url() { let result = DatabaseConfigLoader::new("postgresql://invalid:5432/nonexistent").await; assert!( result.is_err(), "Should fail to connect to invalid database" ); } #[tokio::test] async fn test_load_config_resilience() { let loader = create_loader().await; // Test various invalid strategy IDs let invalid_ids = vec!["", " ", "INVALID", "123", "test-strategy-xyz"]; for id in invalid_ids { let result = loader.load_config(id).await; match result { Ok(None) => { // Expected: strategy not found } Ok(Some(_)) => { panic!("Should not load config for invalid ID: {}", id); } Err(e) => { // Acceptable: database error for malformed ID eprintln!("Database error for ID '{}': {}", id, e); } } } } // ============================================================================ // HELPER TESTS // ============================================================================ #[tokio::test] async fn test_database_connection_reuse() { // Verify we can create multiple loaders and load configs let loader1 = create_loader().await; let loader2 = create_loader().await; let config1 = loader1 .load_config("default-production") .await .expect("Failed to load with loader1"); let config2 = loader2 .load_config("default-production") .await .expect("Failed to load with loader2"); assert!(config1.is_some()); assert!(config2.is_some()); } #[test] fn test_config_type_conversions() { // Test enum conversions use adaptive_strategy::config_types::{ ExecutionAlgorithm, PositionSizingMethod, RegimeDetectionMethod, }; // PositionSizingMethod let kelly = PositionSizingMethod::from_str("KELLY").expect("Failed to parse KELLY"); assert_eq!(kelly, PositionSizingMethod::Kelly); assert_eq!(kelly.to_db_string(), "KELLY"); // RegimeDetectionMethod let hmm = RegimeDetectionMethod::from_str("HMM").expect("Failed to parse HMM"); assert_eq!(hmm, RegimeDetectionMethod::HMM); assert_eq!(hmm.to_db_string(), "HMM"); // ExecutionAlgorithm let twap = ExecutionAlgorithm::from_str("TWAP").expect("Failed to parse TWAP"); assert_eq!(twap, ExecutionAlgorithm::TWAP); assert_eq!(twap.to_db_string(), "TWAP"); }