//! Comprehensive algorithm tests for adaptive-strategy crate //! //! This test suite provides extensive coverage for: //! - Strategy signal generation algorithms //! - Position sizing logic (Kelly, PPO, risk parity) //! - Ensemble model coordination and voting //! - Performance tracking and metrics //! - Model factory and registry operations //! - Regime detection and adaptation //! //! Target: Increase coverage from 40-50% → 75%+ use adaptive_strategy::config::{ AdaptiveStrategyConfig, ExecutionAlgorithm, ModelConfig, PositionSizingMethod, RegimeDetectionMethod, }; use adaptive_strategy::ensemble::EnsembleCoordinator; use adaptive_strategy::models::{ModelFactory, ModelMetadata, ModelRegistry, TrainingData}; use adaptive_strategy::risk::{ KellyConfig, KellyPositionSizer, PortfolioRiskMetrics, PositionRiskMetrics, PositionSizeRecommendation, RiskManager, }; use adaptive_strategy::{AdaptiveStrategy, PerformanceMetrics, StrategyState}; use common::{MarketRegime, Position}; use rust_decimal::Decimal; use std::collections::HashMap; // ============================================================================ // STRATEGY ALGORITHM TESTS (10 tests) // ============================================================================ #[tokio::test] async fn test_adaptive_strategy_creation_with_default_config() { let config = AdaptiveStrategyConfig::default(); let strategy = AdaptiveStrategy::new(config).await; assert!(strategy.is_ok(), "Strategy creation should succeed"); let strategy = strategy.unwrap(); let state = strategy.get_state().await; assert!(!state.active, "Initial strategy should be inactive"); assert_eq!(state.current_regime, "unknown"); } #[tokio::test] async fn test_adaptive_strategy_state_transitions() { let config = AdaptiveStrategyConfig::default(); let strategy = AdaptiveStrategy::new(config).await.unwrap(); // Initial state let initial_state = strategy.get_state().await; assert!(!initial_state.active); // Strategy lifecycle would be tested here if start() didn't run indefinitely // In production, we'd use a mock or time-limited execution } #[tokio::test] async fn test_strategy_performance_metrics_initialization() { let metrics = PerformanceMetrics::default(); assert_eq!(metrics.sharpe_ratio, 0.0); assert_eq!(metrics.max_drawdown, 0.0); assert_eq!(metrics.total_return, 0.0); assert_eq!(metrics.win_rate, 0.0); assert_eq!(metrics.trade_count, 0); } #[tokio::test] async fn test_strategy_config_update() { let config = AdaptiveStrategyConfig::default(); let mut strategy = AdaptiveStrategy::new(config.clone()).await.unwrap(); // Create modified config let mut new_config = config; new_config.risk.max_leverage = 3.0; let result = strategy.update_config(new_config).await; assert!(result.is_ok(), "Config update should succeed"); } #[tokio::test] async fn test_strategy_state_serialization() { let state = StrategyState { active: true, current_regime: "bull".to_string(), model_weights: HashMap::from([("mamba2".to_string(), 0.4), ("tlob".to_string(), 0.6)]), last_update: chrono::Utc::now(), performance: PerformanceMetrics::default(), }; let serialized = serde_json::to_string(&state); assert!(serialized.is_ok(), "State should be serializable"); let deserialized: Result = serde_json::from_str(&serialized.unwrap()); assert!(deserialized.is_ok(), "State should be deserializable"); } #[tokio::test] async fn test_strategy_with_kelly_position_sizing() { let mut config = AdaptiveStrategyConfig::default(); config.risk.position_sizing_method = PositionSizingMethod::Kelly; config.risk.kelly_fraction = 0.25; let strategy = AdaptiveStrategy::new(config).await; assert!( strategy.is_ok(), "Strategy with Kelly sizing should create successfully" ); } #[tokio::test] async fn test_strategy_with_ppo_position_sizing() { let mut config = AdaptiveStrategyConfig::default(); config.risk.position_sizing_method = PositionSizingMethod::PPO; let strategy = AdaptiveStrategy::new(config).await; assert!( strategy.is_ok(), "Strategy with PPO sizing should create successfully" ); } #[tokio::test] async fn test_strategy_with_custom_execution_algorithm() { let mut config = AdaptiveStrategyConfig::default(); config.execution.algorithm = ExecutionAlgorithm::VWAP; config.execution.max_slippage_bps = 5.0; let strategy = AdaptiveStrategy::new(config).await; assert!( strategy.is_ok(), "Strategy with VWAP should create successfully" ); } #[tokio::test] async fn test_strategy_with_hmm_regime_detection() { let mut config = AdaptiveStrategyConfig::default(); config.regime.detection_method = RegimeDetectionMethod::HMM; config.regime.lookback_window = 252; let strategy = AdaptiveStrategy::new(config).await; assert!( strategy.is_ok(), "Strategy with HMM regime detection should succeed" ); } #[tokio::test] async fn test_strategy_with_multiple_models() { let mut config = AdaptiveStrategyConfig::default(); config.ensemble.models = vec![ ModelConfig { id: "mamba2".to_string(), name: "mamba2_model".to_string(), model_type: "mamba2".to_string(), parameters: serde_json::Value::Null, initial_weight: 0.25, enabled: true, }, ModelConfig { id: "tlob".to_string(), name: "tlob_model".to_string(), model_type: "tlob".to_string(), parameters: serde_json::Value::Null, initial_weight: 0.25, enabled: true, }, ModelConfig { id: "lstm".to_string(), name: "lstm_model".to_string(), model_type: "lstm".to_string(), parameters: serde_json::Value::Null, initial_weight: 0.25, enabled: true, }, ModelConfig { id: "transformer".to_string(), name: "transformer_model".to_string(), model_type: "transformer".to_string(), parameters: serde_json::Value::Null, initial_weight: 0.25, enabled: true, }, ]; let strategy = AdaptiveStrategy::new(config).await; assert!( strategy.is_ok(), "Strategy with 4 models should create successfully" ); } // ============================================================================ // POSITION SIZING ALGORITHM TESTS (10 tests) // ============================================================================ #[tokio::test] async fn test_kelly_position_sizer_creation() { let config = KellyConfig { max_fraction: 0.25, min_fraction: 0.01, lookback_period: 252, confidence_threshold: 0.6, volatility_adjustment: true, drawdown_protection: true, dynamic_risk_scaling: true, max_concentration: 0.20, correlation_adjustment: 0.85, base_kelly: 0.25, }; let sizer = KellyPositionSizer::new(config); assert!(sizer.is_ok(), "Kelly sizer should create successfully"); } #[tokio::test] async fn test_kelly_position_sizing_calculation() { let config = KellyConfig::default(); let mut sizer = KellyPositionSizer::new(config).unwrap(); let historical_returns = vec![ 0.05, -0.02, 0.08, -0.03, 0.06, -0.01, 0.04, -0.02, 0.07, -0.01, 0.03, -0.04, 0.09, -0.02, 0.05, -0.03, ]; let mut market_data = adaptive_strategy::risk::MarketData { prices: HashMap::new(), volatilities: HashMap::new(), correlations: HashMap::new(), timestamp: chrono::Utc::now(), volatility_index: Some(20.0), sentiment_indicators: HashMap::new(), }; market_data.prices.insert("AAPL".to_string(), 150.0); market_data.volatilities.insert("AAPL".to_string(), 0.25); let recommendation = sizer .calculate_position_size("AAPL", 0.08, 0.75, &historical_returns, &market_data) .await; assert!(recommendation.is_ok(), "Kelly calculation should succeed"); let rec = recommendation.unwrap(); assert!(rec.recommended_fraction >= 0.0 && rec.recommended_fraction <= 0.25); assert!(rec.confidence > 0.0 && rec.confidence <= 1.0); } #[tokio::test] async fn test_kelly_with_high_volatility_regime() { let config = KellyConfig::default(); let mut sizer = KellyPositionSizer::new(config).unwrap(); // Update to high volatility regime sizer .update_market_regime(adaptive_strategy::regime::MarketRegime::HighVolatility) .await .unwrap(); let historical_returns = vec![0.05; 20]; let mut market_data = adaptive_strategy::risk::MarketData { prices: HashMap::new(), volatilities: HashMap::new(), correlations: HashMap::new(), timestamp: chrono::Utc::now(), volatility_index: Some(40.0), // High VIX sentiment_indicators: HashMap::new(), }; market_data.prices.insert("SPY".to_string(), 450.0); market_data.volatilities.insert("SPY".to_string(), 0.35); // High volatility let recommendation = sizer .calculate_position_size("SPY", 0.05, 0.7, &historical_returns, &market_data) .await .unwrap(); // In high volatility, Kelly should recommend smaller positions assert!( recommendation.recommended_fraction < 0.15, "High volatility should reduce position size" ); } #[tokio::test] async fn test_kelly_with_drawdown_protection() { let config = KellyConfig { drawdown_protection: true, max_fraction: 0.25, ..Default::default() }; let sizer = KellyPositionSizer::new(config); assert!(sizer.is_ok()); // Drawdown protection should reduce position sizes during losses // This would be tested with actual drawdown tracking in production } #[tokio::test] async fn test_fixed_fractional_position_sizing() { let mut config = AdaptiveStrategyConfig::default(); config.risk.position_sizing_method = PositionSizingMethod::FixedFractional(0.1); let mut risk_manager = RiskManager::new(config.risk).unwrap(); let recommendation = risk_manager .calculate_position_size("MSFT", 0.06, 0.8, 300.0) .await; assert!(recommendation.is_ok()); let rec = recommendation.unwrap(); assert!(rec.size > 0.0, "Should recommend non-zero position"); } #[tokio::test] async fn test_risk_parity_position_sizing() { let mut config = AdaptiveStrategyConfig::default(); config.risk.position_sizing_method = PositionSizingMethod::RiskParity; let mut risk_manager = RiskManager::new(config.risk).unwrap(); let recommendation = risk_manager .calculate_position_size("GOOGL", 0.07, 0.85, 140.0) .await; assert!(recommendation.is_ok()); } #[tokio::test] async fn test_volatility_target_position_sizing() { let mut config = AdaptiveStrategyConfig::default(); config.risk.position_sizing_method = PositionSizingMethod::VolatilityTarget; let mut risk_manager = RiskManager::new(config.risk).unwrap(); let recommendation = risk_manager .calculate_position_size("TSLA", 0.10, 0.65, 250.0) .await; assert!(recommendation.is_ok()); } #[tokio::test] async fn test_position_size_with_risk_limits() { let mut config = AdaptiveStrategyConfig::default(); config.risk.max_position_size = 0.05; // 5% limit config.risk.max_leverage = 1.5; let mut risk_manager = RiskManager::new(config.risk).unwrap(); let recommendation = risk_manager .calculate_position_size("NVDA", 0.15, 0.9, 500.0) .await .unwrap(); // Position should be constrained by risk limits assert!( recommendation.size <= recommendation.max_allowed_size, "Position should not exceed max allowed" ); } #[tokio::test] async fn test_position_sizing_risk_metrics() { let config = AdaptiveStrategyConfig::default(); let mut risk_manager = RiskManager::new(config.risk).unwrap(); let recommendation = risk_manager .calculate_position_size("AMZN", 0.08, 0.75, 180.0) .await .unwrap(); // Verify risk metrics are calculated assert!(recommendation.risk_metrics.expected_return > 0.0); assert!(recommendation.risk_metrics.expected_volatility >= 0.0); assert!(recommendation.risk_metrics.var_95 >= 0.0); assert!(recommendation.risk_metrics.cvar_95 >= recommendation.risk_metrics.var_95); } #[tokio::test] async fn test_position_size_recommendation_serialization() { let recommendation = PositionSizeRecommendation { size: 100.0, confidence: 0.85, max_allowed_size: 150.0, method: "Kelly".to_string(), risk_metrics: PositionRiskMetrics { expected_return: 0.08, expected_volatility: 0.20, sharpe_ratio: 0.4, var_95: 20.0, cvar_95: 25.0, max_loss: 100.0, }, timestamp: chrono::Utc::now(), }; let serialized = serde_json::to_string(&recommendation); assert!(serialized.is_ok()); let deserialized: Result = serde_json::from_str(&serialized.unwrap()); assert!(deserialized.is_ok()); } // ============================================================================ // ENSEMBLE MODEL COORDINATION TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_ensemble_coordinator_creation() { let config = AdaptiveStrategyConfig::default(); let coordinator = EnsembleCoordinator::new(&config).await; assert!( coordinator.is_ok(), "Ensemble coordinator should create successfully" ); } #[tokio::test] async fn test_ensemble_prediction_generation() { let config = AdaptiveStrategyConfig::default(); let coordinator = EnsembleCoordinator::new(&config).await.unwrap(); let features = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let horizon = chrono::Duration::seconds(60); let prediction = coordinator.predict(&features, horizon).await; assert!(prediction.is_ok(), "Ensemble prediction should succeed"); let pred = prediction.unwrap(); assert!( pred.confidence >= 0.0 && pred.confidence <= 1.0, "Confidence should be in [0,1]" ); assert!( !pred.model_contributions.is_empty(), "Should have model contributions" ); } #[tokio::test] async fn test_ensemble_weight_updates() { let config = AdaptiveStrategyConfig::default(); let coordinator = EnsembleCoordinator::new(&config).await.unwrap(); let initial_weights = coordinator.get_weights().await; assert!(!initial_weights.is_empty(), "Should have initial weights"); // Update weights let result = coordinator.update_weights_default().await; assert!(result.is_ok(), "Weight update should succeed"); let updated_weights = coordinator.get_weights().await; assert_eq!(initial_weights.len(), updated_weights.len()); } #[tokio::test] async fn test_ensemble_outcome_recording() { let config = AdaptiveStrategyConfig::default(); let coordinator = EnsembleCoordinator::new(&config).await.unwrap(); let timestamp = chrono::Utc::now(); let actual_outcome = 0.05; let result = coordinator .record_outcome_legacy(timestamp, actual_outcome) .await; assert!(result.is_ok(), "Outcome recording should succeed"); } #[tokio::test] async fn test_ensemble_performance_tracking() { let config = AdaptiveStrategyConfig::default(); let coordinator = EnsembleCoordinator::new(&config).await.unwrap(); let performance = coordinator.get_performance().await; assert_eq!( performance.accuracy().len(), 0, "Initial accuracy should be empty" ); } // ============================================================================ // MODEL FACTORY AND REGISTRY TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_model_factory_available_models() { let models = ModelFactory::available_models(); assert!(models.contains(&"lstm"), "Should support LSTM"); assert!( models.contains(&"transformer"), "Should support Transformer" ); assert!( models.contains(&"random_forest"), "Should support Random Forest" ); assert!(models.contains(&"xgboost"), "Should support XGBoost"); assert!(models.contains(&"tlob"), "Should support TLOB"); } #[tokio::test] async fn test_model_factory_creation() { let config = adaptive_strategy::models::ModelConfig::default(); let model = ModelFactory::create_model("lstm", "test_lstm".to_string(), config).await; assert!(model.is_ok(), "LSTM model creation should succeed"); let model = model.unwrap(); assert_eq!(model.name(), "test_lstm"); assert_eq!(model.model_type(), "lstm"); } #[tokio::test] async fn test_model_registry_operations() { let mut registry = ModelRegistry::new(); assert_eq!( registry.list_models().len(), 0, "Registry should start empty" ); let config = adaptive_strategy::models::ModelConfig::default(); let model = ModelFactory::create_model("mock", "test_model".to_string(), config) .await .unwrap(); registry.register(model); assert_eq!(registry.list_models().len(), 1); assert!(registry.get("test_model").is_some()); let removed = registry.remove("test_model"); assert!(removed.is_some()); assert_eq!(registry.list_models().len(), 0); } #[tokio::test] async fn test_model_training_data_validation() { let features = vec![vec![1.0, 2.0, 3.0]; 10]; let targets = vec![0.5; 10]; let feature_names = vec!["f1".to_string(), "f2".to_string(), "f3".to_string()]; let data = TrainingData::new(features, targets, feature_names); assert!(data.validate().is_ok(), "Valid data should pass validation"); assert_eq!(data.len(), 10); assert!(!data.is_empty()); } #[tokio::test] async fn test_model_training_data_invalid() { let features = vec![vec![1.0, 2.0]; 5]; let targets = vec![0.5; 3]; // Mismatched length let feature_names = vec!["f1".to_string(), "f2".to_string()]; let data = TrainingData::new(features, targets, feature_names); assert!( data.validate().is_err(), "Invalid data should fail validation" ); } // ============================================================================ // RISK MANAGEMENT INTEGRATION TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_risk_manager_position_update() { let config = AdaptiveStrategyConfig::default(); let mut risk_manager = RiskManager::new(config.risk).unwrap(); let position = Position { id: uuid::Uuid::new_v4(), symbol: "AAPL".into(), quantity: Decimal::from(100), avg_price: Decimal::from(150), avg_cost: Decimal::from(150), basis: Decimal::from(15000), average_price: Decimal::from(150), market_value: Decimal::from(15000), unrealized_pnl: Decimal::ZERO, realized_pnl: Decimal::ZERO, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), last_updated: chrono::Utc::now(), current_price: Some(Decimal::from(155)), notional_value: Decimal::from(15500), margin_requirement: Decimal::from(1550), }; let result = risk_manager.update_position(position).await; assert!(result.is_ok(), "Position update should succeed"); } #[tokio::test] async fn test_risk_manager_portfolio_metrics() { let config = AdaptiveStrategyConfig::default(); let risk_manager = RiskManager::new(config.risk).unwrap(); let metrics = risk_manager.get_portfolio_risk_metrics().await; assert!(metrics.is_ok(), "Portfolio metrics should be calculated"); let metrics = metrics.unwrap(); assert!(metrics.portfolio_var >= 0.0); assert!(metrics.leverage >= 0.0); assert!(metrics.current_drawdown >= 0.0); } #[tokio::test] async fn test_risk_manager_trade_risk_check() { let config = AdaptiveStrategyConfig::default(); let risk_manager = RiskManager::new(config.risk).unwrap(); let passes = risk_manager.check_trade_risk("MSFT", 50.0, 300.0).await; assert!(passes.is_ok(), "Trade risk check should execute"); } #[tokio::test] async fn test_risk_manager_limits_status() { let config = AdaptiveStrategyConfig::default(); let risk_manager = RiskManager::new(config.risk).unwrap(); let status = risk_manager.get_risk_limits_status().await; assert!(status.is_ok(), "Should retrieve risk limits status"); let status = status.unwrap(); assert!(status.contains_key("leverage") || status.contains_key("drawdown")); } #[tokio::test] async fn test_risk_manager_market_regime_update() { let config = AdaptiveStrategyConfig::default(); let mut risk_manager = RiskManager::new(config.risk).unwrap(); let result = risk_manager.update_market_regime(MarketRegime::Bull).await; assert!(result.is_ok(), "Market regime update should succeed"); let result = risk_manager .update_market_regime(MarketRegime::HighVolatility) .await; assert!( result.is_ok(), "High volatility regime update should succeed" ); } // ============================================================================ // PERFORMANCE METRICS AND TRACKING TESTS (5 tests) // ============================================================================ #[tokio::test] async fn test_portfolio_risk_metrics_serialization() { let metrics = PortfolioRiskMetrics { portfolio_var: 0.015, portfolio_cvar: 0.019, leverage: 1.8, current_drawdown: 0.03, max_drawdown: 0.05, sharpe_ratio: 1.5, sortino_ratio: 1.8, beta: Some(0.95), concentration_risk: 0.15, timestamp: chrono::Utc::now(), }; let serialized = serde_json::to_string(&metrics); assert!(serialized.is_ok()); let deserialized: Result = serde_json::from_str(&serialized.unwrap()); assert!(deserialized.is_ok()); } #[tokio::test] async fn test_position_risk_metrics_calculation() { let metrics = PositionRiskMetrics { expected_return: 0.08, expected_volatility: 0.20, sharpe_ratio: 0.4, var_95: 1000.0, cvar_95: 1280.0, max_loss: 5000.0, }; assert!(metrics.sharpe_ratio > 0.0); assert!(metrics.cvar_95 > metrics.var_95, "CVaR should exceed VaR"); assert!( metrics.max_loss >= metrics.cvar_95, "Max loss should be highest" ); } #[tokio::test] async fn test_performance_metrics_updates() { let mut metrics = PerformanceMetrics::default(); // Simulate trading results metrics.sharpe_ratio = 1.8; metrics.max_drawdown = 0.05; metrics.total_return = 0.25; metrics.win_rate = 0.62; metrics.trade_count = 150; assert!(metrics.sharpe_ratio > 0.0); assert!(metrics.win_rate > 0.5); assert!(metrics.trade_count > 0); } #[tokio::test] async fn test_metadata_creation() { let metadata = ModelMetadata { name: "test_model".to_string(), model_type: "lstm".to_string(), version: "1.0.0".to_string(), created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), parameters: HashMap::new(), input_dimensions: 128, description: Some("Test model".to_string()), }; assert_eq!(metadata.name, "test_model"); assert_eq!(metadata.input_dimensions, 128); } #[tokio::test] async fn test_concentration_metrics() { let config = AdaptiveStrategyConfig::default(); let risk_manager = RiskManager::new(config.risk).unwrap(); let concentration = risk_manager.get_concentration_metrics().await; // May be None if Kelly sizer not initialized assert!(concentration.is_ok()); }