//! Comprehensive tests for autonomous capital-based scaling //! //! Tests cover: //! - Tier selection based on capital //! - System constraint validation //! - Performance-based auto-adjustment //! - Universe reselection on tier changes //! - Database persistence use bigdecimal::BigDecimal; use chrono::Utc; use sqlx::PgPool; use std::str::FromStr; use trading_agent_service::autonomous_scaling::{ AutonomousUniverseManager, CapitalScalingTier, PerformanceMetrics, PositionSizingMode, ScalingError, SystemConstraints, }; /// Helper to create test database pool async fn create_test_pool() -> PgPool { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); PgPool::connect(&database_url) .await .expect("Failed to connect to test database") } /// Helper to clean up test data async fn cleanup_test_data(pool: &PgPool) { sqlx::query("DELETE FROM autonomous_scaling_config WHERE current_tier = 999") .execute(pool) .await .ok(); sqlx::query("DELETE FROM scaling_tier_history WHERE reason LIKE 'TEST:%'") .execute(pool) .await .ok(); } #[tokio::test] async fn test_tier_selection_for_different_capitals() { // Tier 1: $10K-$49K let tier = CapitalScalingTier::for_capital(25_000.0).unwrap(); assert_eq!(tier.tier, 1); assert_eq!(tier.max_symbols, 3); assert_eq!(tier.position_sizing, PositionSizingMode::EqualWeight); // Tier 2: $50K-$99K let tier = CapitalScalingTier::for_capital(75_000.0).unwrap(); assert_eq!(tier.tier, 2); assert_eq!(tier.max_symbols, 6); assert_eq!(tier.position_sizing, PositionSizingMode::MLOptimized); // Tier 3: $100K-$249K let tier = CapitalScalingTier::for_capital(150_000.0).unwrap(); assert_eq!(tier.tier, 3); assert_eq!(tier.max_symbols, 12); assert_eq!(tier.position_sizing, PositionSizingMode::RiskParity); // Tier 6: $1M+ let tier = CapitalScalingTier::for_capital(2_500_000.0).unwrap(); assert_eq!(tier.tier, 6); assert_eq!(tier.max_symbols, 50); assert_eq!(tier.position_sizing, PositionSizingMode::BlackLitterman); } #[tokio::test] async fn test_tier_boundaries() { // Exact boundaries let tier = CapitalScalingTier::for_capital(10_000.0).unwrap(); assert_eq!(tier.tier, 1); let tier = CapitalScalingTier::for_capital(50_000.0).unwrap(); assert_eq!(tier.tier, 2); let tier = CapitalScalingTier::for_capital(100_000.0).unwrap(); assert_eq!(tier.tier, 3); let tier = CapitalScalingTier::for_capital(250_000.0).unwrap(); assert_eq!(tier.tier, 4); let tier = CapitalScalingTier::for_capital(500_000.0).unwrap(); assert_eq!(tier.tier, 5); let tier = CapitalScalingTier::for_capital(1_000_000.0).unwrap(); assert_eq!(tier.tier, 6); // Below minimum assert!(CapitalScalingTier::for_capital(5_000.0).is_none()); } #[tokio::test] async fn test_system_constraints_latency_budget() { let constraints = SystemConstraints::default(); // 3 symbols: 3 * 15ms = 45ms < 100ms ✓ assert!(constraints.can_handle_symbols(3).is_ok()); // 6 symbols: 6 * 15ms = 90ms < 100ms ✓ assert!(constraints.can_handle_symbols(6).is_ok()); // 7 symbols: 7 * 15ms = 105ms > 100ms ✗ let result = constraints.can_handle_symbols(7); assert!(result.is_err()); if let Err(ScalingError::ConstraintViolation(msg)) = result { assert!(msg.contains("Latency budget exceeded")); } } #[tokio::test] async fn test_system_constraints_memory_budget() { let constraints = SystemConstraints { max_ml_latency: 10000, // Disable latency check ..SystemConstraints::default() }; // 3 symbols: 6 * 3 * 50MB = 900MB < 8GB ✓ assert!(constraints.can_handle_symbols(3).is_ok()); // 20 symbols: 6 * 20 * 50MB = 6GB < 8GB ✓ assert!(constraints.can_handle_symbols(20).is_ok()); // 30 symbols: 6 * 30 * 50MB = 9GB > 8GB ✗ let result = constraints.can_handle_symbols(30); assert!(result.is_err()); if let Err(ScalingError::ConstraintViolation(msg)) = result { assert!(msg.contains("Memory budget exceeded")); } } #[tokio::test] async fn test_system_constraints_rebalance_limit() { let constraints = SystemConstraints { max_ml_latency: 10000, // Disable latency check max_memory_gb: 100.0, // Disable memory check ..SystemConstraints::default() }; // Within limit assert!(constraints.can_handle_symbols(25).is_ok()); // At limit assert!(constraints.can_handle_symbols(30).is_ok()); // Over limit let result = constraints.can_handle_symbols(31); assert!(result.is_err()); if let Err(ScalingError::ConstraintViolation(msg)) = result { assert!(msg.contains("Rebalance load exceeded")); } } #[tokio::test] async fn test_select_optimal_universe_tier1() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Tier 1: $25K → 3 symbols let instruments = manager.select_optimal_universe(25_000.0).await.unwrap(); assert_eq!(instruments.len(), 3); assert!(instruments.iter().all(|i| i.liquidity_score >= 0.85)); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_select_optimal_universe_tier2() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Tier 2: $75K → 6 symbols let instruments = manager.select_optimal_universe(75_000.0).await.unwrap(); assert_eq!(instruments.len(), 6); assert!(instruments.iter().all(|i| i.liquidity_score >= 0.8)); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_select_optimal_universe_invalid_capital() { let pool = create_test_pool().await; let manager = AutonomousUniverseManager::new(pool.clone()); // Zero capital let result = manager.select_optimal_universe(0.0).await; assert!(matches!(result, Err(ScalingError::InvalidCapital(_)))); // Negative capital let result = manager.select_optimal_universe(-1000.0).await; assert!(matches!(result, Err(ScalingError::InvalidCapital(_)))); // Below minimum tier let result = manager.select_optimal_universe(5_000.0).await; assert!(matches!(result, Err(ScalingError::InvalidCapital(_)))); } #[tokio::test] async fn test_config_creation_and_retrieval() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Create config let config = manager.get_or_create_config().await.unwrap(); assert_eq!(config.current_tier, 1); assert_eq!(config.current_capital, 10_000.0); assert!(config.enabled); // Retrieve same config let retrieved = manager.get_latest_config().await.unwrap().unwrap(); assert_eq!(retrieved.config_id, config.config_id); assert_eq!(retrieved.current_tier, config.current_tier); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_capital_update_triggers_tier_change() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Start at Tier 1 ($10K) let mut config = manager.get_or_create_config().await.unwrap(); assert_eq!(config.current_tier, 1); // Update capital to Tier 2 threshold ($50K) config = manager.update_capital(50_000.0).await.unwrap(); assert_eq!(config.current_tier, 2); assert_eq!(config.current_capital, 50_000.0); // Update capital to Tier 3 threshold ($100K) config = manager.update_capital(100_000.0).await.unwrap(); assert_eq!(config.current_tier, 3); assert_eq!(config.current_capital, 100_000.0); // Verify tier change was recorded let history = sqlx::query_as::<_, (Option, i32, rust_decimal::Decimal, String)>( r#" SELECT from_tier, to_tier, capital, reason FROM scaling_tier_history WHERE capital::TEXT = $1 ORDER BY timestamp DESC LIMIT 1 "#, ) .bind("100000.00") .fetch_one(&pool) .await .unwrap(); assert_eq!(history.0, Some(2)); assert_eq!(history.1, 3); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_performance_based_downgrade() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Create Tier 2 config with poor performance let mut config = manager.get_or_create_config().await.unwrap(); config.current_tier = 2; config.current_capital = 75_000.0; config.performance_30d = PerformanceMetrics { sharpe_ratio: 0.3, // Below Tier 2 threshold (0.7 * 0.8 = 0.56) total_return_pct: -5.0, max_drawdown_pct: 15.0, win_rate: 0.4, capital_growth_rate: -0.05, num_trades: 100, period_start: Utc::now(), period_end: Utc::now(), }; // Manually store config to test monitoring sqlx::query( r#" INSERT INTO autonomous_scaling_config ( config_id, enabled, current_tier, current_capital, current_symbols, last_rebalance, performance_30d, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (config_id) DO UPDATE SET current_tier = EXCLUDED.current_tier, performance_30d = EXCLUDED.performance_30d, updated_at = EXCLUDED.updated_at "#, ) .bind(config.config_id) .bind(config.enabled) .bind(config.current_tier as i32) .bind(BigDecimal::from_str(&config.current_capital.to_string()).unwrap()) .bind(6i32) .bind(config.last_rebalance) .bind(serde_json::to_value(&config.performance_30d).unwrap()) .bind(config.created_at) .bind(Utc::now()) .execute(&pool) .await .unwrap(); // Monitor should trigger downgrade let event = manager.monitor_and_adjust().await.unwrap(); assert!(event.is_some()); let event = event.unwrap(); assert_eq!(event.from_tier, Some(2)); assert_eq!(event.to_tier, 1); assert!(event.reason.contains("Performance degradation")); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_performance_based_upgrade() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Create Tier 1 config with excellent performance let mut config = manager.get_or_create_config().await.unwrap(); config.current_tier = 1; config.current_capital = 60_000.0; // Above Tier 2 threshold config.performance_30d = PerformanceMetrics { sharpe_ratio: 0.75, // Above Tier 1 threshold (0.5 * 1.2 = 0.6) total_return_pct: 15.0, max_drawdown_pct: 5.0, win_rate: 0.65, capital_growth_rate: 0.15, // 15% growth num_trades: 200, period_start: Utc::now(), period_end: Utc::now(), }; // Manually store config sqlx::query( r#" INSERT INTO autonomous_scaling_config ( config_id, enabled, current_tier, current_capital, current_symbols, last_rebalance, performance_30d, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (config_id) DO UPDATE SET current_tier = EXCLUDED.current_tier, current_capital = EXCLUDED.current_capital, performance_30d = EXCLUDED.performance_30d, updated_at = EXCLUDED.updated_at "#, ) .bind(config.config_id) .bind(config.enabled) .bind(config.current_tier as i32) .bind(BigDecimal::from_str(&config.current_capital.to_string()).unwrap()) .bind(3i32) .bind(config.last_rebalance) .bind(serde_json::to_value(&config.performance_30d).unwrap()) .bind(config.created_at) .bind(Utc::now()) .execute(&pool) .await .unwrap(); // Monitor should trigger upgrade let event = manager.monitor_and_adjust().await.unwrap(); assert!(event.is_some()); let event = event.unwrap(); assert_eq!(event.from_tier, Some(1)); assert_eq!(event.to_tier, 2); assert!(event.reason.contains("Strong performance")); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_monitor_disabled_config() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Create disabled config let mut config = manager.get_or_create_config().await.unwrap(); config.enabled = false; sqlx::query( r#" INSERT INTO autonomous_scaling_config ( config_id, enabled, current_tier, current_capital, current_symbols, last_rebalance, performance_30d, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (config_id) DO UPDATE SET enabled = EXCLUDED.enabled "#, ) .bind(config.config_id) .bind(false) .bind(config.current_tier as i32) .bind(BigDecimal::from_str(&config.current_capital.to_string()).unwrap()) .bind(3i32) .bind(config.last_rebalance) .bind(serde_json::to_value(&config.performance_30d).unwrap()) .bind(config.created_at) .bind(Utc::now()) .execute(&pool) .await .unwrap(); // Monitor should return error let result = manager.monitor_and_adjust().await; assert!(matches!(result, Err(ScalingError::NotEnabled))); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_tier_history_persistence() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Record tier changes manager .record_tier_change(Some(1), 2, 50_000.0, "TEST: Capital increase") .await .unwrap(); manager .record_tier_change(Some(2), 3, 100_000.0, "TEST: Strong performance") .await .unwrap(); manager .record_tier_change(Some(3), 2, 100_000.0, "TEST: Performance degradation") .await .unwrap(); // Verify history let history = sqlx::query_as::<_, (Option, i32, String)>( r#" SELECT from_tier, to_tier, reason FROM scaling_tier_history WHERE reason LIKE 'TEST:%' ORDER BY timestamp ASC "#, ) .fetch_all(&pool) .await .unwrap(); assert_eq!(history.len(), 3); assert_eq!(history[0].0, Some(1)); assert_eq!(history[0].1, 2); assert_eq!(history[1].0, Some(2)); assert_eq!(history[1].1, 3); assert_eq!(history[2].0, Some(3)); assert_eq!(history[2].1, 2); cleanup_test_data(&pool).await; } #[tokio::test] async fn test_custom_constraints() { let pool = create_test_pool().await; // Create manager with tight constraints let constraints = SystemConstraints { max_ml_latency: 50, // 50ms max_order_gen_time: 25, // 25ms max_memory_gb: 4.0, // 4GB max_concurrent_inferences: 18, // 3 models * 6 symbols max_db_connections: 25, max_rebalance_symbols: 10, }; let manager = AutonomousUniverseManager::with_constraints(pool.clone(), constraints.clone()); // 3 symbols: 45ms < 50ms ✓ let instruments = manager.select_optimal_universe(25_000.0).await.unwrap(); assert_eq!(instruments.len(), 3); // 4 symbols: 60ms > 50ms ✗ // (Would be tier 2 with 6 symbols, but constraints prevent it) // For this test, we just verify constraints are enforced assert!(constraints.can_handle_symbols(4).is_err()); } #[tokio::test] async fn test_all_tiers_have_valid_parameters() { let tiers = CapitalScalingTier::all_tiers(); for tier in &tiers { // Verify tier numbers are sequential assert!(tier.tier >= 1 && tier.tier <= 6); // Verify capital thresholds increase if tier.tier > 1 { let prev_tier = &tiers[tier.tier as usize - 2]; assert!(tier.min_capital > prev_tier.min_capital); } // Verify max_symbols increases if tier.tier > 1 { let prev_tier = &tiers[tier.tier as usize - 2]; assert!(tier.max_symbols > prev_tier.max_symbols); } // Verify correlation threshold is valid assert!(tier.max_correlation >= 0.0 && tier.max_correlation <= 1.0); // Verify Sharpe ratio threshold is positive assert!(tier.min_sharpe_ratio > 0.0); } } #[tokio::test] async fn test_concurrent_config_updates() { let pool = create_test_pool().await; cleanup_test_data(&pool).await; let manager = AutonomousUniverseManager::new(pool.clone()); // Create multiple concurrent update tasks let handles: Vec<_> = (1..=5) .map(|i| { let manager = AutonomousUniverseManager::new(pool.clone()); tokio::spawn( async move { manager.update_capital(10_000.0 + i as f64 * 10_000.0).await }, ) }) .collect(); // Wait for all to complete for handle in handles { handle.await.unwrap().unwrap(); } // Verify final state is consistent let config = manager.get_latest_config().await.unwrap().unwrap(); assert!(config.current_capital >= 20_000.0 && config.current_capital <= 60_000.0); cleanup_test_data(&pool).await; }