//! Ensemble Coordinator Integration Test //! //! Tests the full ensemble integration with Trading Service: //! 1. Ensemble initialization with models //! 2. Prediction flow with feature extraction //! 3. Fallback mechanism on ensemble failure //! 4. Health checks and monitoring //! 5. Order execution with ensemble attribution use ml::Features; use std::sync::Arc; use trading_service::ensemble_coordinator::EnsembleCoordinator; #[allow(unused_imports)] use trading_service::state::{EnsembleTradingSignal, TradingActionType}; #[tokio::test] async fn test_ensemble_coordinator_initialization() { // Create ensemble coordinator let coordinator = Arc::new(EnsembleCoordinator::new()); // Verify initialization assert_eq!(coordinator.model_count().await, 0); // Register models coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Verify model count assert_eq!(coordinator.model_count().await, 3); } #[tokio::test] async fn test_ensemble_prediction_flow() { // Create and initialize ensemble coordinator let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Create features let features = Features::new( vec![0.5, 0.6, 0.7, 0.8, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); // Make prediction let decision = coordinator.predict(&features).await.unwrap(); // Verify prediction assert!(decision.confidence >= 0.0 && decision.confidence <= 1.0); assert!(decision.signal >= -1.0 && decision.signal <= 1.0); assert_eq!(decision.model_count(), 3); // Verify model votes assert!(decision.model_votes.contains_key("DQN")); assert!(decision.model_votes.contains_key("PPO")); assert!(decision.model_votes.contains_key("TFT")); } #[tokio::test] async fn test_ensemble_confidence_thresholds() { let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // High confidence features (all positive) let high_conf_features = Features::new( vec![0.9, 0.9, 0.9, 0.9, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let decision = coordinator.predict(&high_conf_features).await.unwrap(); // Should have high confidence assert!(decision.confidence > 0.7); } #[tokio::test] async fn test_ensemble_disagreement_detection() { let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Features that should cause disagreement let features = Features::new( vec![0.1, 0.2, 0.3, 0.4, 0.5], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let decision = coordinator.predict(&features).await.unwrap(); // Verify disagreement rate is tracked assert!(decision.disagreement_rate >= 0.0 && decision.disagreement_rate <= 1.0); } #[tokio::test] async fn test_model_weight_updates() { let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Update weights based on performance coordinator.update_model_weights().await.unwrap(); // Make prediction with updated weights let features = Features::new( vec![0.5, 0.6, 0.7, 0.8, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let decision = coordinator.predict(&features).await.unwrap(); assert!(decision.confidence >= 0.0); } #[tokio::test] async fn test_multiple_predictions() { let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Make 100 predictions for i in 0..100 { let features = Features::new( vec![i as f64 * 0.01, 0.6, 0.7, 0.8, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let decision = coordinator.predict(&features).await.unwrap(); assert!(decision.confidence >= 0.0 && decision.confidence <= 1.0); } } #[tokio::test] async fn test_trading_action_types() { let coordinator = Arc::new(EnsembleCoordinator::new()); coordinator .register_model("DQN".to_string(), 0.35) .await .unwrap(); coordinator .register_model("PPO".to_string(), 0.35) .await .unwrap(); coordinator .register_model("TFT".to_string(), 0.30) .await .unwrap(); // Test different feature ranges to get different actions let test_features = vec![ (vec![0.9, 0.9, 0.9, 0.9, 0.9], "high"), (vec![0.5, 0.5, 0.5, 0.5, 0.5], "medium"), (vec![0.1, 0.1, 0.1, 0.1, 0.1], "low"), ]; for (values, label) in test_features { let features = Features::new( values, vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let decision = coordinator.predict(&features).await.unwrap(); println!( "Features ({}): action={:?}, signal={:.3}", label, decision.action, decision.signal ); // All actions should be valid match decision.action { ml::ensemble::TradingAction::Buy | ml::ensemble::TradingAction::Sell | ml::ensemble::TradingAction::Hold => {}, } } } #[tokio::test] async fn test_empty_model_registry() { let coordinator = Arc::new(EnsembleCoordinator::new()); // Try prediction with no models let features = Features::new( vec![0.5, 0.6, 0.7, 0.8, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); // Should return error let result = coordinator.predict(&features).await; assert!(result.is_err()); } #[tokio::test] async fn test_position_sizing_calculation() { // Test position sizing logic let base_size: u64 = 100; // High confidence, low disagreement let confidence: f64 = 0.9; let disagreement: f64 = 0.1; let confidence_multiplier = ((confidence - 0.5) * 2.0).clamp(0.0, 1.0); let disagreement_penalty = 1.0 - disagreement; let position_size = (base_size as f64 * confidence_multiplier * disagreement_penalty) as u64; assert!(position_size > 70); // Should be large position // Low confidence, high disagreement let confidence: f64 = 0.55; let disagreement: f64 = 0.8; let confidence_multiplier = ((confidence - 0.5) * 2.0).clamp(0.0, 1.0); let disagreement_penalty = 1.0 - disagreement; let position_size = (base_size as f64 * confidence_multiplier * disagreement_penalty) as u64; assert!(position_size < 20); // Should be small position } #[test] fn test_trading_action_conversion() { use trading_service::state::TradingActionType; // Test all action types exist and are distinct let buy = TradingActionType::Buy; let sell = TradingActionType::Sell; let hold = TradingActionType::Hold; assert_ne!(buy, sell); assert_ne!(buy, hold); assert_ne!(sell, hold); } #[tokio::test] async fn test_ensemble_metrics_recording() { use trading_service::ensemble_metrics::EnsemblePredictionMetrics; let metrics = EnsemblePredictionMetrics { symbol: "ES.FUT".to_string(), action: "buy".to_string(), confidence: 0.85, disagreement_rate: 0.25, aggregation_latency_us: 35.0, aggregation_method: "weighted_average".to_string(), }; // Should not panic metrics.record(); }