#![allow( dead_code, unused_variables, clippy::manual_range_contains, clippy::needless_update )] //! TDD Tests for A/B Testing Pipeline //! //! This test suite validates the automated A/B testing pipeline for model deployment decisions. //! //! ## Test Coverage //! 1. A/B test creation on model deployment //! 2. Traffic splitting (50/50 control vs treatment) //! 3. Metrics collection (Sharpe, win rate, drawdown) //! 4. Statistical testing (t-test, p < 0.05) //! 5. Deployment decision logic //! 6. Rollback on failure use anyhow::Result; use sqlx::PgPool; use uuid::Uuid; use trading_service::ab_testing_pipeline::{ ABTestingConfig, ABTestingPipeline, DeploymentDecision, ModelPerformanceMetrics, }; /// Test helper: Create test database pool async fn create_test_pool() -> Result { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url).await?; Ok(pool) } /// Test helper: Clean up test data async fn cleanup_test_data(pool: &PgPool, test_id: &str) -> Result<()> { sqlx::query("DELETE FROM ab_test_results WHERE test_id LIKE $1") .bind(format!("{}%", test_id)) .execute(pool) .await?; Ok(()) } // ============================================================================ // TEST HELPERS MODULE // ============================================================================ /// Test helper module for A/B testing pipeline tests mod test_helpers { use super::*; /// Create test A/B configuration with 50/50 split /// /// # Arguments /// * `prefix` - Test ID prefix for namespacing /// /// # Returns /// ABTestingConfig with test-friendly defaults (min_sample_size=100) /// /// # Example /// ``` /// let config = test_helpers::create_test_ab_config("my_test"); /// assert_eq!(config.traffic_split, 0.5); // 50/50 split /// assert_eq!(config.min_sample_size, 100); // Fast for tests /// ``` pub fn create_test_ab_config(prefix: &str) -> ABTestingConfig { ABTestingConfig { test_prefix: prefix.to_string(), min_sample_size: 100, // Lower for faster tests traffic_split: 0.5, // 50/50 control vs treatment significance_level: 0.05, // p < 0.05 max_duration_hours: 24, // 1 day } } /// Create test A/B configuration with custom parameters /// /// # Arguments /// * `prefix` - Test ID prefix /// * `min_sample_size` - Minimum samples per group /// * `traffic_split` - Traffic split ratio (0.0 to 1.0) /// /// # Example /// ``` /// let config = test_helpers::create_custom_ab_config("test", 200, 0.7); /// assert_eq!(config.traffic_split, 0.7); // 70/30 split /// ``` pub fn create_custom_ab_config( prefix: &str, min_sample_size: usize, traffic_split: f64, ) -> ABTestingConfig { ABTestingConfig { test_prefix: prefix.to_string(), min_sample_size, traffic_split, significance_level: 0.05, max_duration_hours: 24, } } /// Generate realistic mock metrics for A/B testing /// /// # Arguments /// * `predictions` - Total number of predictions /// * `win_rate` - Win rate (0.0 to 1.0) /// * `sharpe_ratio` - Annualized Sharpe ratio /// /// # Returns /// ModelPerformanceMetrics with calculated PnL and drawdown /// /// # Example /// ``` /// let metrics = test_helpers::generate_mock_metrics(1000, 0.55, 1.2); /// assert_eq!(metrics.predictions, 1000); /// assert_eq!(metrics.win_rate, 0.55); /// assert_eq!(metrics.sharpe_ratio, 1.2); /// ``` pub fn generate_mock_metrics( predictions: u64, win_rate: f64, sharpe_ratio: f64, ) -> ModelPerformanceMetrics { let correct_predictions = (predictions as f64 * win_rate) as u64; let avg_pnl = sharpe_ratio * 100.0; // Rough estimate let total_pnl = avg_pnl * predictions as f64; let max_drawdown = if sharpe_ratio < 0.0 { total_pnl.abs() * 0.3 } else { 0.0 }; ModelPerformanceMetrics { predictions, correct_predictions, win_rate, total_pnl, avg_pnl, sharpe_ratio, max_drawdown, avg_latency_us: 50.0, // Default latency } } /// Builder pattern for mock metrics with fine-grained control /// /// # Example /// ``` /// let metrics = MockMetricsBuilder::new(1000) /// .with_win_rate(0.65) /// .with_sharpe(1.8) /// .with_latency(35.0) /// .build(); /// ``` pub struct MockMetricsBuilder { predictions: u64, correct_predictions: u64, win_rate: f64, total_pnl: f64, avg_pnl: f64, sharpe_ratio: f64, max_drawdown: f64, avg_latency_us: f64, } impl MockMetricsBuilder { pub fn new(predictions: u64) -> Self { Self { predictions, correct_predictions: 0, win_rate: 0.5, total_pnl: 0.0, avg_pnl: 0.0, sharpe_ratio: 0.0, max_drawdown: 0.0, avg_latency_us: 50.0, } } pub fn with_win_rate(mut self, win_rate: f64) -> Self { self.win_rate = win_rate; self.correct_predictions = (self.predictions as f64 * win_rate) as u64; self } pub fn with_sharpe(mut self, sharpe_ratio: f64) -> Self { self.sharpe_ratio = sharpe_ratio; self.avg_pnl = sharpe_ratio * 100.0; self.total_pnl = self.avg_pnl * self.predictions as f64; self } pub fn with_latency(mut self, latency_us: f64) -> Self { self.avg_latency_us = latency_us; self } pub fn with_max_drawdown(mut self, max_drawdown: f64) -> Self { self.max_drawdown = max_drawdown; self } pub fn build(self) -> ModelPerformanceMetrics { ModelPerformanceMetrics { predictions: self.predictions, correct_predictions: self.correct_predictions, win_rate: self.win_rate, total_pnl: self.total_pnl, avg_pnl: self.avg_pnl, sharpe_ratio: self.sharpe_ratio, max_drawdown: self.max_drawdown, avg_latency_us: self.avg_latency_us, } } } /// Assert that deployment decision is to rollout treatment /// /// # Example /// ``` /// let decision = pipeline.make_deployment_decision(&test_id).await.unwrap(); /// test_helpers::assert_rollout_decision(&decision, 0.2); /// ``` #[track_caller] pub fn assert_rollout_decision(decision: &DeploymentDecision, min_improvement: f64) { match decision { DeploymentDecision::RolloutTreatment { sharpe_improvement, .. } => { assert!( *sharpe_improvement >= min_improvement, "Sharpe improvement {} below threshold {}", sharpe_improvement, min_improvement ); }, _ => panic!("Expected RolloutTreatment, got {:?}", decision), } } /// Assert that deployment decision is to revert to control #[track_caller] pub fn assert_revert_decision(decision: &DeploymentDecision) { match decision { DeploymentDecision::RevertToControl { sharpe_degradation, .. } => { assert!( *sharpe_degradation < 0.0, "Expected negative Sharpe degradation" ); }, _ => panic!("Expected RevertToControl, got {:?}", decision), } } /// Assert that deployment decision is neutral #[track_caller] pub fn assert_neutral_decision(decision: &DeploymentDecision) { assert!( matches!(decision, DeploymentDecision::Neutral { .. }), "Expected Neutral decision, got {:?}", decision ); } /// Assert that deployment decision is inconclusive /// /// # Arguments /// * `decision` - The deployment decision to check /// * `expected_reason` - Substring expected in the reason message #[track_caller] pub fn assert_inconclusive_decision(decision: &DeploymentDecision, expected_reason: &str) { match decision { DeploymentDecision::Inconclusive { reason, .. } => { assert!( reason.contains(expected_reason), "Expected reason containing '{}', got '{}'", expected_reason, reason ); }, _ => panic!("Expected Inconclusive, got {:?}", decision), } } } // ============================================================================ // EXISTING TESTS // ============================================================================ /// Test 1: Create A/B test on model deployment #[tokio::test] async fn test_create_ab_test_on_deployment() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_create_{}", Uuid::new_v4()); // Create pipeline let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, traffic_split: 0.5, significance_level: 0.05, max_duration_hours: 24, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); // Simulate model deployment event let control_model_id = "DQN_v1.0.0"; let treatment_model_id = "DQN_v2.0.0"; let result = pipeline .create_ab_test(control_model_id, treatment_model_id, "ES.FUT") .await; // Should create test successfully assert!( result.is_ok(), "Failed to create A/B test: {:?}", result.err() ); let test_state = result.unwrap(); assert_eq!(test_state.control_model, control_model_id); assert_eq!(test_state.treatment_model, treatment_model_id); assert_eq!(test_state.status, "running"); // Cleanup cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 2: Traffic splitting (50/50 control vs treatment) #[tokio::test] async fn test_traffic_splitting_50_50() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_split_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), traffic_split: 0.5, // 50/50 ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); // Create test let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Simulate 1000 predictions with deterministic assignment let mut control_count = 0; let mut treatment_count = 0; for i in 0..1000 { let user_id = format!("user_{}", i); let group = pipeline .assign_traffic_group(&test_state.test_id, &user_id) .await .unwrap(); match group.as_str() { "control" => control_count += 1, "treatment" => treatment_count += 1, _ => panic!("Unknown group: {}", group), } } // Should be close to 50/50 (within 10% tolerance) let control_ratio = control_count as f64 / 1000.0; assert!( control_ratio >= 0.40 && control_ratio <= 0.60, "Traffic split not balanced: {}% control", control_ratio * 100.0 ); cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 3: Metrics collection (Sharpe, win rate, drawdown) #[tokio::test] async fn test_metrics_collection() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_metrics_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Record prediction outcomes for control group for i in 0..150 { let pnl = if i % 2 == 0 { 100.0 } else { -50.0 }; // 50% win rate, positive PnL let return_pct = pnl / 10000.0; pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, // correct pnl, return_pct, 50, // latency_us ) .await .unwrap(); } // Record prediction outcomes for treatment group (better performance) for i in 0..150 { let pnl = if i % 3 != 0 { 120.0 } else { -40.0 }; // 66% win rate, higher PnL let return_pct = pnl / 10000.0; pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 3 != 0, // correct pnl, return_pct, 45, // latency_us (faster) ) .await .unwrap(); } // Get metrics let metrics = pipeline .get_ab_test_metrics(&test_state.test_id) .await .unwrap(); // Validate control metrics assert_eq!(metrics.control.predictions, 150); assert!(metrics.control.win_rate >= 0.48 && metrics.control.win_rate <= 0.52); // ~50% assert!(metrics.control.total_pnl > 0.0); // Positive PnL assert!(metrics.control.sharpe_ratio > 0.0); // Positive Sharpe // Validate treatment metrics (should be better) assert_eq!(metrics.treatment.predictions, 150); assert!(metrics.treatment.win_rate >= 0.64 && metrics.treatment.win_rate <= 0.68); // ~66% assert!(metrics.treatment.total_pnl > metrics.control.total_pnl); // Higher PnL assert!(metrics.treatment.sharpe_ratio > metrics.control.sharpe_ratio); // Higher Sharpe cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 4: Statistical testing (t-test, p < 0.05) #[tokio::test] async fn test_statistical_significance_testing() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_stats_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, significance_level: 0.05, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Record control group (baseline performance) for i in 0..120 { let return_pct = 0.001; // Low return pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, return_pct * 10000.0, return_pct, 50, ) .await .unwrap(); } // Record treatment group (significantly better) for i in 0..120 { let return_pct = 0.003; // 3x higher return pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 3 != 0, return_pct * 10000.0, return_pct, 45, ) .await .unwrap(); } // Run statistical tests let test_result = pipeline .run_statistical_tests(&test_state.test_id) .await .unwrap(); // Should detect significant difference assert!( test_result.sharpe_test.is_significant, "Sharpe difference not significant" ); assert!( test_result.sharpe_test.p_value < 0.05, "P-value too high: {}", test_result.sharpe_test.p_value ); // Should have positive effect assert!( test_result.sharpe_diff > 0.0, "Treatment not better than control" ); cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 5: Deployment decision logic (rollout on success) #[tokio::test] async fn test_deployment_decision_rollout() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_deploy_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Control: baseline for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, 0.001 * 10000.0, 0.001, 50, ) .await .unwrap(); } // Treatment: significantly better for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 3 != 0, 0.004 * 10000.0, 0.004, 40, ) .await .unwrap(); } // Make deployment decision let decision = pipeline .make_deployment_decision(&test_state.test_id) .await .unwrap(); // Should recommend rollout match decision { DeploymentDecision::RolloutTreatment { reason, .. } => { assert!( reason.contains("outperforms"), "Unexpected reason: {}", reason ); }, _ => panic!("Expected RolloutTreatment, got {:?}", decision), } cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 6: Deployment decision logic (rollback on failure) #[tokio::test] async fn test_deployment_decision_rollback() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_rollback_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Control: good baseline for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 != 0, // 50% win rate 0.003 * 10000.0, 0.003, 50, ) .await .unwrap(); } // Treatment: significantly worse for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 3 == 0, // 33% win rate -0.001 * 10000.0, // Negative PnL -0.001, 60, // Slower ) .await .unwrap(); } // Make deployment decision let decision = pipeline .make_deployment_decision(&test_state.test_id) .await .unwrap(); // Should recommend rollback match decision { DeploymentDecision::RevertToControl { reason, .. } => { assert!( reason.contains("underperforms"), "Unexpected reason: {}", reason ); }, _ => panic!("Expected RevertToControl, got {:?}", decision), } cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 7: Deployment decision logic (neutral - continue testing) #[tokio::test] async fn test_deployment_decision_neutral() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_neutral_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Both groups have identical performance for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, 0.002 * 10000.0, 0.002, 50, ) .await .unwrap(); pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 2 == 0, 0.002 * 10000.0, 0.002, 50, ) .await .unwrap(); } // Make deployment decision let decision = pipeline .make_deployment_decision(&test_state.test_id) .await .unwrap(); // Should be neutral or inconclusive match decision { DeploymentDecision::Neutral { .. } | DeploymentDecision::Inconclusive { .. } => { // Expected }, _ => panic!("Expected Neutral or Inconclusive, got {:?}", decision), } cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 8: Insufficient samples handling #[tokio::test] async fn test_insufficient_samples() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_insufficient_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), min_sample_size: 100, ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Record only 50 samples (below minimum) for i in 0..50 { pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, 0.002 * 10000.0, 0.002, 50, ) .await .unwrap(); } // Try to make decision with insufficient samples let result = pipeline.make_deployment_decision(&test_state.test_id).await; // Should return Inconclusive due to insufficient samples match result { Ok(DeploymentDecision::Inconclusive { reason, control_samples, treatment_samples, required_samples, }) => { assert!( reason.contains("insufficient") || reason.contains("samples"), "Unexpected reason: {}", reason ); assert!( control_samples < required_samples || treatment_samples < required_samples, "Expected insufficient samples, but got control={}, treatment={}, required={}", control_samples, treatment_samples, required_samples ); }, other => panic!("Expected Inconclusive error, got {:?}", other), } cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 9: Deterministic traffic assignment (same user always gets same group) #[tokio::test] async fn test_deterministic_traffic_assignment() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_deterministic_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Same user should always get same group let user_id = "user_123"; let group1 = pipeline .assign_traffic_group(&test_state.test_id, user_id) .await .unwrap(); let group2 = pipeline .assign_traffic_group(&test_state.test_id, user_id) .await .unwrap(); let group3 = pipeline .assign_traffic_group(&test_state.test_id, user_id) .await .unwrap(); assert_eq!(group1, group2); assert_eq!(group2, group3); cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 10: Integration with ensemble predictions #[tokio::test] async fn test_integration_with_ensemble_predictions() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_integration_{}", Uuid::new_v4()); let config = ABTestingConfig { test_prefix: test_id.clone(), ..Default::default() }; let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Insert mock ensemble prediction let prediction_id = Uuid::new_v4(); sqlx::query( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate, timestamp ) VALUES ($1, $2, $3, $4, $5, $6, NOW()) "#, ) .bind(prediction_id) .bind("ES.FUT") .bind("BUY") .bind(0.8) .bind(0.75) .bind(0.1) .execute(&pool) .await .unwrap(); // Assign traffic group for this prediction let user_id = prediction_id.to_string(); let group = pipeline .assign_traffic_group(&test_state.test_id, &user_id) .await .unwrap(); // Record outcome pipeline .record_prediction_outcome(&test_state.test_id, &group, true, 150.0, 0.0015, 42) .await .unwrap(); // Verify metrics updated let metrics = pipeline .get_ab_test_metrics(&test_state.test_id) .await .unwrap(); if group == "control" { assert_eq!(metrics.control.predictions, 1); } else { assert_eq!(metrics.treatment.predictions, 1); } // Cleanup sqlx::query("DELETE FROM ensemble_predictions WHERE id = $1") .bind(prediction_id) .execute(&pool) .await .unwrap(); cleanup_test_data(&pool, &test_id).await.unwrap(); } // ============================================================================ // EXAMPLE TESTS DEMONSTRATING TEST HELPERS // ============================================================================ /// Test 11: Example using all test helpers #[tokio::test] async fn test_example_using_all_helpers() { let pool = create_test_pool() .await .expect("Failed to create test pool"); let test_id = format!("test_helpers_example_{}", Uuid::new_v4()); // Use configuration factory for 50/50 split let config = test_helpers::create_test_ab_config(&test_id); assert_eq!(config.traffic_split, 0.5); assert_eq!(config.min_sample_size, 100); let pipeline = ABTestingPipeline::new(pool.clone(), config); let test_state = pipeline .create_ab_test("DQN_v1.0.0", "DQN_v2.0.0", "ES.FUT") .await .unwrap(); // Simulate control group with baseline metrics for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "control", i % 2 == 0, 0.001 * 10000.0, 0.001, 50, ) .await .unwrap(); } // Simulate treatment group with better metrics for i in 0..120 { pipeline .record_prediction_outcome( &test_state.test_id, "treatment", i % 3 != 0, 0.003 * 10000.0, 0.003, 45, ) .await .unwrap(); } // Make deployment decision let decision = pipeline .make_deployment_decision(&test_state.test_id) .await .unwrap(); // Use assertion helper to validate rollout decision test_helpers::assert_rollout_decision(&decision, 0.0); cleanup_test_data(&pool, &test_id).await.unwrap(); } /// Test 12: Using MockMetricsBuilder for fine-grained control #[tokio::test] async fn test_mock_metrics_builder() { // Use builder pattern for complex metrics let control_metrics = test_helpers::MockMetricsBuilder::new(1000) .with_win_rate(0.52) .with_sharpe(1.0) .with_latency(50.0) .build(); let treatment_metrics = test_helpers::MockMetricsBuilder::new(1000) .with_win_rate(0.68) .with_sharpe(1.8) .with_latency(42.0) .with_max_drawdown(0.05) .build(); // Validate metrics assert_eq!(control_metrics.predictions, 1000); assert_eq!(control_metrics.win_rate, 0.52); assert_eq!(control_metrics.sharpe_ratio, 1.0); assert_eq!(control_metrics.avg_latency_us, 50.0); assert_eq!(treatment_metrics.predictions, 1000); assert_eq!(treatment_metrics.win_rate, 0.68); assert_eq!(treatment_metrics.sharpe_ratio, 1.8); assert_eq!(treatment_metrics.avg_latency_us, 42.0); assert_eq!(treatment_metrics.max_drawdown, 0.05); // Treatment should be better assert!(treatment_metrics.sharpe_ratio > control_metrics.sharpe_ratio); assert!(treatment_metrics.win_rate > control_metrics.win_rate); } /// Test 13: Using generate_mock_metrics for quick setup #[tokio::test] async fn test_generate_mock_metrics_quick() { // Quick metrics generation let baseline = test_helpers::generate_mock_metrics(500, 0.50, 0.8); let improved = test_helpers::generate_mock_metrics(500, 0.65, 1.5); assert_eq!(baseline.predictions, 500); assert_eq!(baseline.win_rate, 0.50); assert_eq!(baseline.sharpe_ratio, 0.8); assert_eq!(improved.predictions, 500); assert_eq!(improved.win_rate, 0.65); assert_eq!(improved.sharpe_ratio, 1.5); // Improved model should have better metrics assert!(improved.total_pnl > baseline.total_pnl); } /// Test 14: Custom configuration with different traffic split #[tokio::test] async fn test_custom_config_70_30_split() { let config = test_helpers::create_custom_ab_config("test_70_30", 200, 0.7); assert_eq!(config.traffic_split, 0.7); // 70% control, 30% treatment assert_eq!(config.min_sample_size, 200); }