#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! TDD E2E Integration Tests for ML Trading Pipeline //! //! **Mission**: Comprehensive end-to-end tests for ML trading pipeline using strict TDD methodology //! **Methodology**: RED (Failing Tests) → GREEN (Minimal Implementation) → REFACTOR (Quality) //! //! ## Test Coverage //! 1. End-to-end ML trading pipeline (data → features → prediction → order → tracking) //! 2. Ensemble consensus voting with disagreement handling //! 3. Fallback to rule-based on low confidence //! 4. Multi-symbol trading with ML predictions //! 5. Performance tracking (accuracy, Sharpe ratio) //! 6. Risk limits override ML signals //! 7. Model comparison across 4 models //! //! ## TDD Protocol //! - **RED Phase**: All tests are `#[ignore]` and WILL FAIL //! - **GREEN Phase**: Remove `#[ignore]` and implement minimal code to pass //! - **REFACTOR Phase**: Improve code quality without changing behavior #![allow(unused_imports)] use anyhow::{anyhow, Result}; use ml_core::native_types::NativeDevice; use common::{CommonError, OrderSide, OrderType}; use sqlx::PgPool; use std::collections::HashMap; use std::path::PathBuf; use uuid::Uuid; // Import trading service ML components use trading_service::{ ml_performance_metrics::MLMetricsStore, Action, EnsembleCoordinator, Order, PaperTradingExecutor, SignalSource, TradingSignal, }; // Import rand for random testing use rand; // ============================================================================ // Test Infrastructure & Helper Functions // ============================================================================ /// Create test database pool async fn get_test_db_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") } /// Create test ensemble coordinator with all 4 models (DQN, PPO, MAMBA2, TFT) fn create_test_ensemble() -> std::sync::Arc { use std::sync::Arc; let coordinator = Arc::new(EnsembleCoordinator::new()); // Note: In real usage, models would be loaded and registered with the coordinator // For tests, we create a minimal ensemble coordinator without loaded models coordinator } /// Create test ensemble with low confidence (for fallback testing) fn create_test_ensemble_low_confidence() -> std::sync::Arc { // Same as above, but prediction will be mocked to return low confidence create_test_ensemble() } /// Create single-model coordinator (for model comparison tests) fn create_single_model_coordinator(model: &str) -> std::sync::Arc { use std::sync::Arc; let coordinator = Arc::new(EnsembleCoordinator::new()); // Note: In real usage, only the specified model would be loaded // For tests, we create a minimal ensemble coordinator coordinator } /// Load test OHLCV data (50 bars for feature extraction) fn load_test_ohlcv_data(_symbol: &str, num_bars: usize) -> Vec<(f64, f64, f64, f64, f64)> { // Generate synthetic OHLCV data with realistic pattern let mut data = Vec::new(); let mut base_price = 4500.0; // ES.FUT starting price for i in 0..num_bars { let trend = (i as f64 * 0.1).sin(); // Add sine wave trend let open = base_price + trend * 10.0; let high = open + (i as f64 % 5.0) + 5.0; let low = open - (i as f64 % 3.0) - 3.0; let close = open + trend * 5.0; let volume = 1000.0 + (i as f64 * 10.0); data.push((open, high, low, close, volume)); base_price = close; // Next bar starts from previous close } data } /// Load test data with model disagreement (divergent trends) fn load_test_data_with_disagreement() -> Vec<(f64, f64, f64, f64, f64)> { // Generate data that creates model disagreement let mut data = Vec::new(); let mut base_price = 4500.0; for i in 0..50 { // Create choppy market with no clear trend let noise = ((i * 7) % 13) as f64 * 2.0 - 13.0; let open = base_price + noise; let high = open + (i as f64 % 3.0) + 3.0; let low = open - (i as f64 % 2.0) - 2.0; let close = open + noise * 0.3; let volume = 1000.0 + (i as f64 * 5.0); data.push((open, high, low, close, volume)); base_price = close; } data } // ============================================================================ // TEST 1: End-to-End ML Trading Pipeline (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_e2e_ml_trading_pipeline() { // RED: End-to-end test from feature extraction to order execution let pool = get_test_db_pool().await; // 1. Load real market data let market_data = load_test_ohlcv_data("ES.FUT", 50); assert_eq!(market_data.len(), 50, "Need 50 OHLCV bars"); // 2. Create ML engine (feature extraction happens inside PaperTradingExecutor) let ensemble = create_test_ensemble(); // Note: Feature extraction is now handled internally by PaperTradingExecutor // using ml::features::UnifiedFeatureExtractor (256-dim features) // 3. Execute paper trading order let mut executor = PaperTradingExecutor::new_with_ml(pool.clone(), ensemble) .await .expect("Failed to create executor with ML"); // Generate ML signal (includes feature extraction internally) let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate ML signal"); assert!( signal.confidence >= 0.0, "Signal should have valid confidence" ); // 4. Execute order based on signal let order = executor .execute_ml_signal(&signal, "ES.FUT") .await .expect("Failed to execute ML signal"); // Verify order created assert_ne!(order.id, Uuid::nil()); assert_eq!(order.symbol, "ES.FUT"); // 5. Verify prediction stored in database let prediction = sqlx::query!( "SELECT * FROM ml_predictions WHERE order_id = $1 ORDER BY id DESC LIMIT 1", order.id ) .fetch_one(&pool) .await .expect("Failed to fetch prediction"); assert_eq!(prediction.symbol, "ES.FUT"); assert!((prediction.confidence as f64 - ensemble.confidence).abs() < 0.01); // 6. Simulate outcome and record executor .record_outcome(order.id, 150.0) .await .expect("Failed to record outcome"); // +$150 profit // 7. Verify performance metrics updated let metrics_store = MLMetricsStore::new(pool); let stats = metrics_store .get_accuracy_stats("Ensemble") .await .expect("Failed to get accuracy stats"); assert_eq!(stats.total_predictions, 1); assert_eq!(stats.correct_predictions, 1); assert!((stats.accuracy - 1.0).abs() < 0.01); } // ============================================================================ // TEST 2: Ensemble Consensus Voting with Disagreement (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_ensemble_consensus() { // RED: Test ensemble voting with disagreement let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); // Load market data where models disagree let market_data = load_test_data_with_disagreement(); let mut executor = PaperTradingExecutor::new_with_ml(pool, ensemble) .await .expect("Failed to create executor"); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); // Ensemble should use weighted voting assert!(signal.model_votes.is_some(), "Should have model votes"); let votes = signal.model_votes.unwrap(); // At least 3/4 models should agree for high confidence let action_val = signal.action.expect("Should have action") as usize; let consensus_count = votes .iter() .filter(|(_, action, _)| *action == action_val) .count(); if signal.confidence > 0.8 { assert!( consensus_count >= 3, "High confidence requires 3+ model agreement, got {}/{}", consensus_count, votes.len() ); } } // ============================================================================ // TEST 3: Fallback to Rule-Based on Low Confidence (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_fallback_on_low_confidence() { // RED: Test fallback to rule-based when confidence < 0.6 let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let mut executor = PaperTradingExecutor::new_with_ml(pool, ensemble) .await .expect("Failed to create executor"); // Disable ML to force fallback executor.disable_ml().await; let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_signal(&market_data) .await .expect("Failed to generate signal"); assert_eq!( signal.source, SignalSource::RuleBased, "Source should be RuleBased" ); assert!(signal.action.is_some(), "Should still generate signal"); } // ============================================================================ // TEST 4: Multi-Symbol ML Trading (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_multi_symbol_trading() { // RED: Test ML predictions for multiple symbols let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let mut executor = PaperTradingExecutor::new_with_ml(pool.clone(), ensemble) .await .expect("Failed to create executor"); let symbols = vec!["ES.FUT", "NQ.FUT", "ZN.FUT"]; for symbol in &symbols { let market_data = load_test_ohlcv_data(symbol, 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); if signal.confidence >= 0.6 { let order = executor .execute_ml_signal(&signal, symbol) .await .expect("Failed to execute signal"); assert_eq!(order.symbol, *symbol); } } // Verify predictions for all symbols let predictions = sqlx::query!("SELECT symbol, COUNT(*) as count FROM ml_predictions GROUP BY symbol") .fetch_all(&pool) .await .expect("Failed to fetch predictions"); assert!( predictions.len() >= 1, "At least 1 symbol should have predictions" ); } // ============================================================================ // TEST 5: ML Performance Tracking - Accuracy Calculation (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_performance_tracking_accuracy() { // RED: Test accuracy calculation with mixed outcomes let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let mut executor = PaperTradingExecutor::new_with_ml(pool.clone(), ensemble) .await .expect("Failed to create executor"); // Execute 10 ML trades for i in 0..10 { let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); let order = executor .execute_ml_signal(&signal, "ES.FUT") .await .expect("Failed to execute signal"); // Record outcome: 7 correct, 3 incorrect let pnl = if i < 7 { 100.0 } else { -50.0 }; executor .record_outcome(order.id, pnl) .await .expect("Failed to record outcome"); } // Verify accuracy metrics let metrics_store = MLMetricsStore::new(pool); let stats = metrics_store .get_accuracy_stats("Ensemble") .await .expect("Failed to get accuracy stats"); assert_eq!(stats.total_predictions, 10); assert_eq!(stats.correct_predictions, 7); assert!((stats.accuracy - 0.7).abs() < 0.01); } // ============================================================================ // TEST 6: Sharpe Ratio Calculation (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_sharpe_ratio_calculation() { // RED: Test Sharpe ratio with profit/loss series let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let mut executor = PaperTradingExecutor::new_with_ml(pool.clone(), ensemble) .await .expect("Failed to create executor"); // Execute trades with varying P&L let pnls = vec![100.0, -50.0, 200.0, -30.0, 150.0, 80.0, -20.0, 120.0]; for pnl in pnls { let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); let order = executor .execute_ml_signal(&signal, "ES.FUT") .await .expect("Failed to execute signal"); executor .record_outcome(order.id, pnl) .await .expect("Failed to record outcome"); } // Calculate Sharpe ratio let metrics_store = MLMetricsStore::new(pool); let sharpe = metrics_store .calculate_sharpe_ratio("Ensemble") .await .expect("Failed to calculate Sharpe ratio"); // Sharpe > 0 means profitable with controlled risk assert!(sharpe > 0.0, "Sharpe ratio should be positive"); // Annualized Sharpe > 1.0 is good if sharpe > 1.0 { println!("✅ Good Sharpe ratio: {:.2}", sharpe); } } // ============================================================================ // TEST 7: Risk Limits Override ML Signals (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_risk_limits_override() { // RED: Test that risk limits override ML signals let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let mut executor = PaperTradingExecutor::new_with_ml(pool, ensemble) .await .expect("Failed to create executor"); // Set strict position limit executor .set_position_limit("ES.FUT", 5) .await .expect("Failed to set position limit"); // Execute 5 trades (hit limit) for _ in 0..5 { let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); executor .execute_ml_signal(&signal, "ES.FUT") .await .expect("Failed to execute signal"); } // 6th trade should be rejected let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); let result = executor.execute_ml_signal(&signal, "ES.FUT").await; assert!( result.is_err(), "6th trade should be rejected due to position limit" ); let error_msg = result.unwrap_err().to_string(); assert!( error_msg.to_lowercase().contains("position") || error_msg.to_lowercase().contains("limit"), "Error should mention position limit, got: {}", error_msg ); } // ============================================================================ // TEST 8: Model Comparison Across 4 Models (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_ml_model_comparison() { // RED: Test comparing performance across 4 models let pool = get_test_db_pool().await; // Execute trades with each model individually for model in &["DQN", "PPO", "MAMBA2", "TFT"] { let ensemble = create_single_model_coordinator(model); let mut executor = PaperTradingExecutor::new_with_ml(pool.clone(), ensemble) .await .expect("Failed to create executor"); for _ in 0..5 { let market_data = load_test_ohlcv_data("ES.FUT", 50); let signal = executor .generate_ml_signal(&market_data) .await .expect("Failed to generate signal"); let order = executor .execute_ml_signal(&signal, "ES.FUT") .await .expect("Failed to execute signal"); // Random outcome for testing let pnl = if rand::random::() > 0.5 { 100.0 } else { -50.0 }; executor .record_outcome(order.id, pnl) .await .expect("Failed to record outcome"); } } // Compare model performance let metrics_store = MLMetricsStore::new(pool); let comparison = metrics_store .compare_model_accuracy() .await .expect("Failed to compare model accuracy"); assert_eq!(comparison.len(), 4, "Should have all 4 models"); // Models should be ranked by accuracy for i in 1..comparison.len() { assert!( comparison[i - 1].1 >= comparison[i].1, "Models should be sorted by accuracy" ); } } // ============================================================================ // TEST 9: Position Sizing Based on Confidence (RED Phase) // ============================================================================ #[tokio::test] #[ignore = "RED: This test will fail until implementation is complete"] async fn test_position_sizing_confidence_mapping() { // RED: Test position sizing scales with confidence let pool = get_test_db_pool().await; let ensemble = create_test_ensemble(); let executor = PaperTradingExecutor::new_with_ml(pool, ensemble) .await .expect("Failed to create executor"); use trading_service::paper_trading_executor::{Action, SignalSource, TradingSignal}; // High confidence signal (0.9) let high_conf_signal = TradingSignal { action: Some(Action::Buy), confidence: 0.9, source: SignalSource::ML, model_votes: None, }; // Low confidence signal (0.6) let low_conf_signal = TradingSignal { action: Some(Action::Buy), confidence: 0.6, source: SignalSource::ML, model_votes: None, }; // Convert both to orders let high_conf_order = executor .convert_signal_to_order(&high_conf_signal, "ES.FUT") .await .expect("High confidence order failed"); let low_conf_order = executor .convert_signal_to_order(&low_conf_signal, "ES.FUT") .await .expect("Low confidence order failed"); // Higher confidence should result in larger position assert!( high_conf_order.quantity > low_conf_order.quantity, "High confidence ({}) should have larger position than low confidence ({})", high_conf_order.quantity, low_conf_order.quantity ); }