//! Unit tests for regime-aware temperature adaptation in DQN //! //! This module tests the integration between RegimeOrchestrator and DQN temperature control. //! Tests cover: //! 1. Temperature adjustment for each regime type (Trending, Ranging, Volatile) //! 2. Integration with RegimeOrchestrator //! 3. Fallback behavior when regime detection unavailable //! 4. Configuration of regime-specific multipliers use chrono::Utc; use ml::regime::orchestrator::{Bar, RegimeOrchestrator, RegimeState}; use sqlx::PgPool; use std::collections::HashMap; /// Helper function to create test bars with trending pattern fn create_trending_bars(count: usize) -> Vec { let base_time = Utc::now(); let base_price = 100.0; (0..count) .map(|i| { let price = base_price + (i as f64 * 0.5); // Strong uptrend Bar { timestamp: base_time + chrono::Duration::seconds(i as i64 * 60), open: price, high: price + 0.3, low: price - 0.2, close: price + 0.25, volume: 10000.0, } }) .collect() } /// Helper function to create test bars with ranging pattern fn create_ranging_bars(count: usize) -> Vec { let base_time = Utc::now(); let base_price = 100.0; (0..count) .map(|i| { // Oscillate between 99-101 let offset = ((i as f64 * 0.5).sin() * 1.0); let price = base_price + offset; Bar { timestamp: base_time + chrono::Duration::seconds(i as i64 * 60), open: price, high: price + 0.2, low: price - 0.2, close: price, volume: 8000.0, } }) .collect() } /// Helper function to create test bars with volatile pattern fn create_volatile_bars(count: usize) -> Vec { let base_time = Utc::now(); let base_price = 100.0; (0..count) .map(|i| { // Large random swings let offset = ((i as f64).sin() * 5.0); // ±5 point swings let price = base_price + offset; Bar { timestamp: base_time + chrono::Duration::seconds(i as i64 * 60), open: price, high: price + 2.0, // Wide range low: price - 2.0, close: price + 0.5, volume: 50000.0, // High volume } }) .collect() } #[tokio::test] async fn test_regime_temperature_multipliers_default() { // Test that default regime temperature multipliers are reasonable let multipliers = get_default_regime_multipliers(); // Verify trending has lower multiplier (exploit trend) assert!(multipliers.get("Trending").unwrap() < &1.0); // Verify ranging has higher multiplier (explore breakouts) assert!(multipliers.get("Ranging").unwrap() >= &1.0); // Verify volatile has high multiplier (high exploration) assert!(multipliers.get("Volatile").unwrap() > &1.0); // Verify normal/fallback regime exists assert!(multipliers.contains_key("Normal")); } #[tokio::test] async fn test_apply_regime_temperature_trending() { // Test temperature adjustment for trending regime let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let regime = "Trending"; let adjusted_temp = apply_regime_temperature(base_temp, regime, &multipliers); // Trending should reduce temperature (0.8x) assert!(adjusted_temp < base_temp); assert!((adjusted_temp - 0.8).abs() < 0.01); } #[tokio::test] async fn test_apply_regime_temperature_ranging() { // Test temperature adjustment for ranging regime let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let regime = "Ranging"; let adjusted_temp = apply_regime_temperature(base_temp, regime, &multipliers); // Ranging should increase temperature (1.2x) assert!(adjusted_temp > base_temp); assert!((adjusted_temp - 1.2).abs() < 0.01); } #[tokio::test] async fn test_apply_regime_temperature_volatile() { // Test temperature adjustment for volatile regime let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let regime = "Volatile"; let adjusted_temp = apply_regime_temperature(base_temp, regime, &multipliers); // Volatile should significantly increase temperature (1.5x) assert!(adjusted_temp > base_temp); assert!((adjusted_temp - 1.5).abs() < 0.01); } #[tokio::test] async fn test_apply_regime_temperature_unknown_fallback() { // Test fallback behavior for unknown regime let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let regime = "UnknownRegime"; let adjusted_temp = apply_regime_temperature(base_temp, regime, &multipliers); // Should fallback to "Normal" multiplier (1.0x) assert!((adjusted_temp - base_temp).abs() < 0.01); } #[tokio::test] #[ignore] // Requires database connection async fn test_regime_orchestrator_integration_trending() { // Test integration with RegimeOrchestrator for trending pattern let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let mut orchestrator = RegimeOrchestrator::new(pool.clone()) .await .expect("Failed to create orchestrator"); // Create trending bars let bars = create_trending_bars(50); // Detect regime let regime_state = orchestrator .detect_and_persist("TEST_SYMBOL_TREND", &bars) .await .expect("Failed to detect regime"); // Verify regime classification (may be "Trending" or "Normal" depending on ADX threshold) assert!( regime_state.regime == "Trending" || regime_state.regime == "Normal", "Unexpected regime: {}", regime_state.regime ); // Apply temperature adjustment let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let adjusted_temp = apply_regime_temperature(base_temp, ®ime_state.regime, &multipliers); // Verify temperature is adjusted based on regime if regime_state.regime == "Trending" { assert!(adjusted_temp < base_temp); } } #[tokio::test] #[ignore] // Requires database connection async fn test_regime_orchestrator_integration_ranging() { // Test integration with RegimeOrchestrator for ranging pattern let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let mut orchestrator = RegimeOrchestrator::new(pool.clone()) .await .expect("Failed to create orchestrator"); // Create ranging bars let bars = create_ranging_bars(50); // Detect regime let regime_state = orchestrator .detect_and_persist("TEST_SYMBOL_RANGE", &bars) .await .expect("Failed to detect regime"); // Verify regime classification assert!( regime_state.regime == "Ranging" || regime_state.regime == "Normal", "Unexpected regime: {}", regime_state.regime ); // Apply temperature adjustment let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let adjusted_temp = apply_regime_temperature(base_temp, ®ime_state.regime, &multipliers); // Verify temperature is adjusted based on regime if regime_state.regime == "Ranging" { assert!(adjusted_temp > base_temp); } } #[tokio::test] #[ignore] // Requires database connection async fn test_regime_orchestrator_integration_volatile() { // Test integration with RegimeOrchestrator for volatile pattern let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url) .await .expect("Failed to connect to database"); let mut orchestrator = RegimeOrchestrator::new(pool.clone()) .await .expect("Failed to create orchestrator"); // Create volatile bars let bars = create_volatile_bars(50); // Detect regime let regime_state = orchestrator .detect_and_persist("TEST_SYMBOL_VOLATILE", &bars) .await .expect("Failed to detect regime"); // Verify regime classification assert!( regime_state.regime == "Volatile" || regime_state.regime == "Normal", "Unexpected regime: {}", regime_state.regime ); // Apply temperature adjustment let base_temp = 1.0; let multipliers = get_default_regime_multipliers(); let adjusted_temp = apply_regime_temperature(base_temp, ®ime_state.regime, &multipliers); // Verify temperature is adjusted based on regime if regime_state.regime == "Volatile" { assert!(adjusted_temp > base_temp); } } #[test] fn test_custom_regime_multipliers() { // Test custom regime multipliers configuration let mut custom_multipliers = HashMap::new(); custom_multipliers.insert("Trending".to_string(), 0.5); // Very low temp custom_multipliers.insert("Ranging".to_string(), 2.0); // Very high temp custom_multipliers.insert("Volatile".to_string(), 1.8); // High temp custom_multipliers.insert("Normal".to_string(), 1.0); // Baseline let base_temp = 1.0; // Test trending let trending_temp = apply_regime_temperature(base_temp, "Trending", &custom_multipliers); assert!((trending_temp - 0.5).abs() < 0.01); // Test ranging let ranging_temp = apply_regime_temperature(base_temp, "Ranging", &custom_multipliers); assert!((ranging_temp - 2.0).abs() < 0.01); // Test volatile let volatile_temp = apply_regime_temperature(base_temp, "Volatile", &custom_multipliers); assert!((volatile_temp - 1.8).abs() < 0.01); } #[test] fn test_temperature_bounds() { // Test that temperature adjustment respects min/max bounds let base_temp = 0.1; // At minimum let multipliers = get_default_regime_multipliers(); // Even with high multiplier, should not go below reasonable bounds let adjusted_temp = apply_regime_temperature(base_temp, "Volatile", &multipliers); // Verify temperature is positive assert!(adjusted_temp > 0.0); // Verify temperature doesn't explode assert!(adjusted_temp < 10.0); } // ============================================================================ // Helper Functions (to be implemented in ml/src/dqn/regime_temperature.rs) // ============================================================================ /// Get default regime temperature multipliers /// /// Returns recommended multipliers based on adaptive temperature research: /// - Trending: 0.8x (exploit trend) /// - Ranging: 1.2x (explore breakouts) /// - Volatile: 1.5x (high exploration) /// - Normal: 1.0x (baseline) fn get_default_regime_multipliers() -> HashMap { let mut multipliers = HashMap::new(); multipliers.insert("Trending".to_string(), 0.8); multipliers.insert("Ranging".to_string(), 1.2); multipliers.insert("Volatile".to_string(), 1.5); multipliers.insert("Normal".to_string(), 1.0); multipliers } /// Apply regime-specific temperature multiplier /// /// # Arguments /// /// * `base_temp` - Base temperature from exponential decay /// * `regime` - Current market regime (Trending, Ranging, Volatile, Normal) /// * `multipliers` - Regime-specific multipliers /// /// # Returns /// /// Adjusted temperature scaled by regime multiplier fn apply_regime_temperature( base_temp: f64, regime: &str, multipliers: &HashMap, ) -> f64 { let multiplier = multipliers.get(regime).unwrap_or(&1.0); base_temp * multiplier }