//! Ensemble integration test -- all 4 models contributing to a single trading decision //! //! Verifies: DQN + PPO + TFT + Mamba2 -> EnsembleCoordinator -> EnsembleDecision //! This is the critical test that proves the ensemble pipeline works end-to-end. use ml::dqn::dqn::DQNConfig; use ml::ensemble::adapters::{ DqnInferenceAdapter, Mamba2InferenceAdapter, PpoInferenceAdapter, TftInferenceAdapter, }; use ml::ensemble::coordinator::EnsembleCoordinator; use ml::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter}; use ml::ensemble::TradingAction; use ml::mamba::Mamba2Config; use ml::ppo::ppo::PPOConfig; use ml::tft::TFTConfig; use ml::Features; fn small_dqn_config() -> DQNConfig { DQNConfig { state_dim: 51, num_actions: 45, hidden_dims: vec![32, 32], ..Default::default() } } fn small_ppo_config() -> PPOConfig { PPOConfig { state_dim: 64, num_actions: 45, policy_hidden_dims: vec![32, 32], value_hidden_dims: vec![32, 32], ..Default::default() } } fn small_tft_config() -> TFTConfig { TFTConfig { input_dim: 20, hidden_dim: 32, num_heads: 2, num_layers: 1, prediction_horizon: 5, sequence_length: 4, num_quantiles: 9, num_static_features: 6, num_known_features: 6, num_unknown_features: 8, dropout_rate: 0.0, ..Default::default() } } fn small_mamba2_config() -> Mamba2Config { Mamba2Config { d_model: 32, d_state: 8, d_head: 8, num_heads: 2, expand: 2, num_layers: 1, max_seq_len: 8, dropout: 0.0, ..Default::default() } } const TFT_SEQ_LEN: usize = 4; const MAMBA2_SEQ_LEN: usize = 4; #[tokio::test] async fn test_ensemble_all_4_models_produce_decision() { // Create all 4 adapters let dqn = DqnInferenceAdapter::new(small_dqn_config()).expect("DQN adapter creation failed"); let ppo = PpoInferenceAdapter::new(small_ppo_config()).expect("PPO adapter creation failed"); let tft = TftInferenceAdapter::new(small_tft_config(), TFT_SEQ_LEN).expect("TFT adapter creation failed"); let mamba2 = Mamba2InferenceAdapter::new(small_mamba2_config(), MAMBA2_SEQ_LEN) .expect("Mamba2 adapter creation failed"); // Warm up TFT and Mamba2 sequence buffers for i in 0..TFT_SEQ_LEN { let fv = FeatureVector { values: vec![0.1; 51], timestamp: 1_700_000_000 + i as i64, }; let _ = tft.predict(&fv); let _ = mamba2.predict(&fv); } // Verify all adapters are ready assert!(dqn.is_ready(), "DQN should be ready"); assert!(ppo.is_ready(), "PPO should be ready"); assert!(tft.is_ready(), "TFT should be ready after warmup"); assert!(mamba2.is_ready(), "Mamba2 should be ready after warmup"); // Build ensemble coordinator let mut coordinator = EnsembleCoordinator::new(); coordinator.add_adapter(Box::new(dqn)); coordinator.add_adapter(Box::new(ppo)); coordinator.add_adapter(Box::new(tft)); coordinator.add_adapter(Box::new(mamba2)); // Register models with equal weights coordinator .register_model("DQN".to_string(), 0.25) .await .expect("register DQN"); coordinator .register_model("PPO".to_string(), 0.25) .await .expect("register PPO"); coordinator .register_model("TFT".to_string(), 0.25) .await .expect("register TFT"); coordinator .register_model("MAMBA-2".to_string(), 0.25) .await .expect("register MAMBA-2"); // Create Features for ensemble prediction let features = Features::new( vec![0.1; 51], (0..51).map(|i| format!("f{}", i)).collect(), ); // Get ensemble decision let decision = coordinator .predict(&features) .await .expect("Ensemble prediction should succeed"); // Verify decision is valid assert!( decision.confidence >= 0.0 && decision.confidence <= 1.0, "ensemble confidence {} out of [0,1]", decision.confidence ); assert!( decision.signal >= -1.0 && decision.signal <= 1.0, "ensemble signal {} out of [-1,1]", decision.signal ); assert!(decision.signal.is_finite(), "signal must not be NaN/Inf"); assert!( decision.confidence.is_finite(), "confidence must not be NaN/Inf" ); // Verify we got votes from all 4 models assert!(!decision.model_votes.is_empty(), "Should have model votes"); // Action should be Buy, Sell, or Hold match decision.action { TradingAction::Buy | TradingAction::Sell | TradingAction::Hold => {} // Valid } println!( "Ensemble decision: action={:?}, signal={:.4}, confidence={:.4}, disagreement={:.4}", decision.action, decision.signal, decision.confidence, decision.disagreement_rate ); for (name, vote) in &decision.model_votes { println!( " {} -> signal={:.4}, confidence={:.4}", name, vote.signal, vote.confidence ); } } #[tokio::test] async fn test_ensemble_no_adapters_returns_error() { let coordinator = EnsembleCoordinator::new(); let features = Features::new(vec![0.1; 51], vec!["f0".to_string()]); let result = coordinator.predict(&features).await; assert!(result.is_err(), "Empty ensemble should return error"); }