//! Wave D: Regime Tracking Database Tests //! //! Tests cover: //! - Regime state insertion and retrieval //! - Regime transitions tracking //! - Adaptive strategy metrics recording //! - Database constraints and validations //! - Helper function correctness use common::database::{DatabasePool, LocalDatabaseConfig}; use sqlx::PgPool; use std::time::Duration; // ============================================================================ // Helper Functions // ============================================================================ /// Create a test database pool for Wave D regime tracking tests async fn create_test_pool() -> DatabasePool { let config = LocalDatabaseConfig { url: "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string(), pool: common::database::PoolConfig { max_connections: 5, min_connections: 2, connect_timeout_ms: 5000, acquire_timeout_ms: 5000, max_lifetime_seconds: 3600, idle_timeout_seconds: 300, }, performance: common::database::PerformanceConfig { query_timeout_micros: 10000, enable_prewarming: false, enable_prepared_statements: true, enable_slow_query_logging: false, slow_query_threshold_micros: 10000, }, }; DatabasePool::new(config) .await .expect("Failed to create test database pool") } /// Clean up test data for a symbol async fn cleanup_test_data(pool: &PgPool, symbol: &str) { let _ = sqlx::query!("DELETE FROM regime_states WHERE symbol = $1", symbol) .execute(pool) .await; let _ = sqlx::query!("DELETE FROM regime_transitions WHERE symbol = $1", symbol) .execute(pool) .await; let _ = sqlx::query!( "DELETE FROM adaptive_strategy_metrics WHERE symbol = $1", symbol ) .execute(pool) .await; } // ============================================================================ // Regime State Tests // ============================================================================ #[tokio::test] async fn test_insert_regime_state() { let pool = create_test_pool().await; let symbol = "TEST.REGIME.STATE"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert regime state let result = pool .insert_regime_state( symbol, "Trending", 0.85, event_timestamp, Some(2.5), Some(-1.2), Some(45.0), Some(0.92), ) .await; assert!( result.is_ok(), "Failed to insert regime state: {:?}", result ); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_get_latest_regime() { let pool = create_test_pool().await; let symbol = "TEST.LATEST.REGIME"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert regime state pool.insert_regime_state( symbol, "Volatile", 0.78, event_timestamp, Some(1.8), Some(-2.1), Some(32.5), Some(0.65), ) .await .expect("Failed to insert regime state"); // Retrieve latest regime let regime = pool .get_latest_regime(symbol) .await .expect("Failed to get latest regime"); assert_eq!(regime.symbol, symbol); assert_eq!(regime.regime, "Volatile"); assert!((regime.confidence - 0.78).abs() < 1e-6); assert!(regime.cusum_s_plus.is_some()); assert!((regime.cusum_s_plus.unwrap() - 1.8).abs() < 1e-6); assert!(regime.adx.is_some()); assert!((regime.adx.unwrap() - 32.5).abs() < 1e-6); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_upsert_regime_state() { let pool = create_test_pool().await; let symbol = "TEST.UPSERT.REGIME"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert initial regime state pool.insert_regime_state( symbol, "Normal", 0.90, event_timestamp, Some(0.5), Some(-0.3), Some(25.0), Some(0.95), ) .await .expect("Failed to insert regime state"); // Upsert with different values (same timestamp) pool.insert_regime_state( symbol, "Trending", 0.92, event_timestamp, Some(3.2), Some(-0.8), Some(55.0), Some(0.88), ) .await .expect("Failed to upsert regime state"); // Retrieve and verify update let regime = pool .get_latest_regime(symbol) .await .expect("Failed to get latest regime"); assert_eq!(regime.regime, "Trending"); assert!((regime.confidence - 0.92).abs() < 1e-6); assert!((regime.cusum_s_plus.unwrap() - 3.2).abs() < 1e-6); assert!((regime.adx.unwrap() - 55.0).abs() < 1e-6); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_regime_state_constraints() { let pool = create_test_pool().await; let symbol = "TEST.CONSTRAINTS"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Test valid regimes let valid_regimes = vec![ "Normal", "Trending", "Ranging", "Volatile", "Crisis", "Illiquid", "Momentum", ]; for (idx, regime) in valid_regimes.iter().enumerate() { let result = pool .insert_regime_state( symbol, regime, 0.80, event_timestamp + chrono::Duration::seconds(i64::try_from(idx).unwrap()), // Unique timestamp None, None, None, None, ) .await; assert!(result.is_ok(), "Failed for valid regime: {}", regime); } cleanup_test_data(pool.pool(), symbol).await; } // ============================================================================ // Regime Transition Tests // ============================================================================ #[tokio::test] async fn test_insert_regime_transition() { let pool = create_test_pool().await; let symbol = "TEST.TRANSITION"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert transition let result = pool .insert_regime_transition( symbol, "Normal", "Trending", event_timestamp, Some(120), Some(0.35), Some(48.5), true, ) .await; assert!( result.is_ok(), "Failed to insert regime transition: {:?}", result ); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_regime_transition_invalid_same_regime() { let pool = create_test_pool().await; let symbol = "TEST.INVALID.TRANSITION"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Attempt invalid transition (same from and to regime) let result = pool .insert_regime_transition( symbol, "Normal", "Normal", // Invalid: same regime event_timestamp, Some(50), Some(0.0), None, false, ) .await; // Should fail due to CHECK constraint assert!( result.is_err(), "Should fail for same-regime transition, but succeeded" ); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_multiple_regime_transitions() { let pool = create_test_pool().await; let symbol = "TEST.MULTIPLE.TRANSITIONS"; cleanup_test_data(pool.pool(), symbol).await; let base_time = chrono::Utc::now(); // Insert multiple transitions let transitions = vec![ ("Normal", "Trending", 100, 0.40), ("Trending", "Volatile", 50, 0.25), ("Volatile", "Normal", 80, 0.35), ]; for (idx, (from, to, duration, prob)) in transitions.iter().enumerate() { let result = pool .insert_regime_transition( symbol, from, to, base_time + chrono::Duration::seconds(i64::try_from(idx).unwrap() * 60), Some(*duration), Some(*prob), None, false, ) .await; assert!( result.is_ok(), "Failed to insert transition {}->{}", from, to ); } cleanup_test_data(pool.pool(), symbol).await; } // ============================================================================ // Adaptive Strategy Metrics Tests // ============================================================================ #[tokio::test] async fn test_upsert_adaptive_strategy_metrics() { let pool = create_test_pool().await; let symbol = "TEST.ADAPTIVE.METRICS"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert initial metrics let result = pool .upsert_adaptive_strategy_metrics( symbol, "Trending", event_timestamp, 1.5, // position_multiplier 2.5, // stop_loss_multiplier Some(1.8), // regime_sharpe Some(0.75), // risk_budget_utilization 10, // total_trades 7, // winning_trades 15000, // total_pnl ) .await; assert!( result.is_ok(), "Failed to insert adaptive strategy metrics: {:?}", result ); // Upsert with additional trades (same timestamp and regime) let result = pool .upsert_adaptive_strategy_metrics( symbol, "Trending", event_timestamp, 1.6, // Updated multiplier 2.6, // Updated stop-loss Some(1.9), // Updated Sharpe Some(0.80), // Updated utilization 5, // Additional trades 3, // Additional wins 7500, // Additional PnL ) .await; assert!(result.is_ok(), "Failed to upsert metrics: {:?}", result); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_adaptive_strategy_metrics_constraints() { let pool = create_test_pool().await; let symbol = "TEST.METRICS.CONSTRAINTS"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Test valid position multipliers (0.0-2.0) let valid_result = pool .upsert_adaptive_strategy_metrics( symbol, "Normal", event_timestamp, 1.0, // Valid 2.0, // Valid None, None, 0, 0, 0, ) .await; assert!(valid_result.is_ok(), "Valid multipliers should succeed"); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_get_regime_performance() { let pool = create_test_pool().await; let symbol = "TEST.PERFORMANCE"; cleanup_test_data(pool.pool(), symbol).await; let event_timestamp = chrono::Utc::now(); // Insert metrics for different regimes pool.upsert_adaptive_strategy_metrics( symbol, "Trending", event_timestamp, 1.5, 2.5, Some(2.1), Some(0.80), 20, 15, 30000, ) .await .expect("Failed to insert Trending metrics"); pool.upsert_adaptive_strategy_metrics( symbol, "Ranging", event_timestamp + chrono::Duration::seconds(1), 0.8, 3.0, Some(1.2), Some(0.50), 15, 8, 12000, ) .await .expect("Failed to insert Ranging metrics"); // Retrieve performance metrics let performance = pool .get_regime_performance(Some(symbol), 24) .await .expect("Failed to get regime performance"); assert!(!performance.is_empty(), "Should have performance data"); assert!( performance.len() >= 2, "Should have metrics for at least 2 regimes" ); cleanup_test_data(pool.pool(), symbol).await; } // ============================================================================ // Integration Tests // ============================================================================ #[tokio::test] async fn test_end_to_end_regime_workflow() { let pool = create_test_pool().await; let symbol = "TEST.E2E.WORKFLOW"; cleanup_test_data(pool.pool(), symbol).await; let base_time = chrono::Utc::now(); // Step 1: Insert initial regime state pool.insert_regime_state( symbol, "Normal", 0.95, base_time, Some(0.2), Some(-0.1), Some(22.0), Some(0.98), ) .await .expect("Failed to insert initial regime"); // Step 2: Insert adaptive strategy metrics for Normal regime pool.upsert_adaptive_strategy_metrics( symbol, "Normal", base_time, 1.0, 2.0, Some(1.5), Some(0.60), 10, 6, 10000, ) .await .expect("Failed to insert Normal metrics"); // Step 3: Transition to Trending regime tokio::time::sleep(Duration::from_millis(100)).await; let transition_time = base_time + chrono::Duration::seconds(60); pool.insert_regime_transition( symbol, "Normal", "Trending", transition_time, Some(120), Some(0.42), Some(52.0), true, ) .await .expect("Failed to insert transition"); // Step 4: Insert new regime state (Trending) pool.insert_regime_state( symbol, "Trending", 0.88, transition_time, Some(3.5), Some(-0.5), Some(52.0), Some(0.85), ) .await .expect("Failed to insert Trending regime"); // Step 5: Insert adaptive strategy metrics for Trending regime pool.upsert_adaptive_strategy_metrics( symbol, "Trending", transition_time, 1.5, 2.5, Some(2.2), Some(0.85), 15, 12, 25000, ) .await .expect("Failed to insert Trending metrics"); // Step 6: Verify latest regime let latest = pool .get_latest_regime(symbol) .await .expect("Failed to get latest regime"); assert_eq!(latest.regime, "Trending"); // Step 7: Verify performance metrics let performance = pool .get_regime_performance(Some(symbol), 24) .await .expect("Failed to get performance"); assert!(!performance.is_empty()); cleanup_test_data(pool.pool(), symbol).await; } #[tokio::test] async fn test_concurrent_regime_updates() { let pool = create_test_pool().await; let symbol = "TEST.CONCURRENT"; cleanup_test_data(pool.pool(), symbol).await; let base_time = chrono::Utc::now(); // Spawn multiple concurrent updates let mut handles = vec![]; for i in 0..5 { let pool_clone = pool.pool().clone(); let symbol_clone = symbol.to_string(); let timestamp = base_time + chrono::Duration::seconds(i64::from(i)); let handle = tokio::spawn(async move { let result = sqlx::query!( r#" INSERT INTO regime_states ( symbol, regime, confidence, event_timestamp, cusum_s_plus, cusum_s_minus, adx, stability ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "#, symbol_clone, "Normal", 0.90, timestamp, Some(0.5), Some(-0.3), Some(25.0), Some(0.95) ) .execute(&pool_clone) .await; result.is_ok() }); handles.push(handle); } // Wait for all updates for handle in handles { let success = handle.await.expect("Task panicked"); assert!(success, "Concurrent update failed"); } cleanup_test_data(pool.pool(), symbol).await; } // ============================================================================ // Database Function Tests // ============================================================================ #[tokio::test] async fn test_get_regime_transition_matrix_function() { let pool = create_test_pool().await; let symbol = "TEST.TRANSITION.MATRIX"; cleanup_test_data(pool.pool(), symbol).await; let base_time = chrono::Utc::now(); // Insert multiple transitions to build a matrix let transitions = vec![ ("Normal", "Trending"), ("Trending", "Volatile"), ("Volatile", "Normal"), ("Normal", "Trending"), // Duplicate to test probability calculation ]; for (idx, (from, to)) in transitions.iter().enumerate() { pool.insert_regime_transition( symbol, from, to, base_time + chrono::Duration::seconds(i64::try_from(idx).unwrap() * 60), Some(100), None, None, false, ) .await .expect("Failed to insert transition"); } // Query transition matrix using database function let matrix = sqlx::query!( r#" SELECT from_regime, to_regime, transition_count, transition_probability FROM get_regime_transition_matrix($1, 24) "#, symbol ) .fetch_all(pool.pool()) .await .expect("Failed to get transition matrix"); assert!(!matrix.is_empty(), "Transition matrix should not be empty"); // Verify Normal->Trending has probability ~0.67 (2 out of 3 transitions from Normal) let normal_trending = matrix.iter().find(|r| { r.from_regime.as_deref() == Some("Normal") && r.to_regime.as_deref() == Some("Trending") }); assert!(normal_trending.is_some()); cleanup_test_data(pool.pool(), symbol).await; }