#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! E2E Test: Ensemble Prediction -> Risk Validation -> Order Execution Pipeline //! //! Mission: Validate complete ML pipeline from prediction to execution with risk checks //! //! ## Pipeline Flow //! //! ```text //! Market Data → Feature Engineering → Ensemble (4 models) → Risk Validation → Order → Execution //! │ │ │ │ │ │ //! ▼ ▼ ▼ ▼ ▼ ▼ //! OHLCV 16 features DQN, PPO, 5 risk checks Database Position //! Bars + 10 indicators MAMBA-2, TFT (all pass) Persist Update //! ``` //! //! ## Risk Validation Checks //! //! 1. ✅ Position Limit Check (current + new ≤ max) //! 2. ✅ Margin Requirement Check (capital available) //! 3. ✅ VaR Check (risk exposure within limits) //! 4. ✅ Symbol Whitelist Check (allowed symbols only) //! 5. ✅ Confidence Threshold Check (≥60% confidence) //! //! ## Performance Targets //! //! - Feature extraction: <50ms //! - Ensemble prediction: <200ms //! - Risk validation: <10ms //! - Order creation: <10ms //! - Execution: <50ms //! - Position update: <10ms //! - Total E2E: <2 seconds use anyhow::{Context, Result}; use sqlx::PgPool; use std::sync::Arc; use std::time::{Duration, Instant}; use uuid::Uuid; // ML and trading imports use common::{OrderSide, OrderStatus}; use ml::ensemble::{EnsembleCoordinator, EnsembleDecision, TradingAction}; use ml::features::FeatureExtractor; use ml::Features; use trading_service::{EnsembleAuditLogger, EnsemblePredictionAudit}; // ============================================================================ // Risk Validation Engine // ============================================================================ #[derive(Debug, Clone)] struct RiskLimits { max_position_size: i32, max_margin_utilization: f64, // Fraction of capital (e.g. 0.5 = 50%) max_var: f64, // Maximum Value-at-Risk in dollars min_confidence: f64, // Minimum ML confidence (e.g. 0.60) allowed_symbols: Vec, } impl Default for RiskLimits { fn default() -> Self { Self { max_position_size: 20, max_margin_utilization: 0.5, max_var: 10_000.0, min_confidence: 0.60, allowed_symbols: vec![ "ES.FUT".to_string(), "NQ.FUT".to_string(), "CL.FUT".to_string(), ], } } } #[derive(Debug)] struct RiskCheckResult { passed: bool, checks: Vec<(String, bool, String)>, // (check_name, passed, message) } impl RiskCheckResult { fn all_passed(&self) -> bool { self.passed && self.checks.iter().all(|(_, passed, _)| *passed) } fn print_summary(&self) { println!(" Risk Validation:"); for (name, passed, message) in &self.checks { let icon = if *passed { "✅" } else { "❌" }; println!(" {} {}: {}", icon, name, message); } } } async fn validate_risk( db_pool: &PgPool, account_id: &str, symbol: &str, new_quantity: i32, price: f64, confidence: f64, limits: &RiskLimits, ) -> Result { let start = Instant::now(); let mut checks = Vec::new(); // Check 1: Position Limit let current_position = sqlx::query( r#" SELECT COALESCE(quantity, 0) as quantity FROM positions WHERE account_id = $1 AND symbol = $2 "#, ) .bind(account_id) .bind(symbol) .fetch_optional(db_pool) .await?; let current_qty = current_position .map(|r| r.get::("quantity")) .unwrap_or(0); let total_qty = current_qty.abs() + new_quantity; let position_check = total_qty <= limits.max_position_size; checks.push(( "Position Limit".to_string(), position_check, format!( "Current: {}, New: {}, Total: {} (limit: {})", current_qty, new_quantity, total_qty, limits.max_position_size ), )); // Check 2: Margin Requirement let order_value = (new_quantity as f64) * price; let available_capital = 100_000.0; // Mock capital for testing let margin_requirement = order_value * 0.15; // 15% margin (ES.FUT typical) let margin_check = margin_requirement <= (available_capital * limits.max_margin_utilization); checks.push(( "Margin Requirement".to_string(), margin_check, format!( "Required: ${:.2}, Available: ${:.2} ({}%)", margin_requirement, available_capital * limits.max_margin_utilization, (limits.max_margin_utilization * 100.0) as i32 ), )); // Check 3: VaR (Value-at-Risk) let var_95 = order_value * 0.05; // Simplified VaR: 5% of position value let var_check = var_95 <= limits.max_var; checks.push(( "VaR (95%)".to_string(), var_check, format!("${:.2} (limit: ${:.2})", var_95, limits.max_var), )); // Check 4: Symbol Whitelist let symbol_check = limits.allowed_symbols.contains(&symbol.to_string()); checks.push(( "Symbol Whitelist".to_string(), symbol_check, format!( "{} {}", symbol, if symbol_check { "✅" } else { "❌ not allowed" } ), )); // Check 5: Confidence Threshold let confidence_check = confidence >= limits.min_confidence; checks.push(( "Confidence Threshold".to_string(), confidence_check, format!( "{:.1}% (min: {:.1}%)", confidence * 100.0, limits.min_confidence * 100.0 ), )); let all_passed = checks.iter().all(|(_, passed, _)| *passed); let risk_duration = start.elapsed(); if risk_duration > Duration::from_millis(10) { println!( " ⚠️ Risk validation took {:?} (target: <10ms)", risk_duration ); } Ok(RiskCheckResult { passed: all_passed, checks, }) } // ============================================================================ // E2E Pipeline Context // ============================================================================ struct E2EPipelineContext { db_pool: PgPool, ensemble: Arc, audit_logger: EnsembleAuditLogger, risk_limits: RiskLimits, account_id: String, } impl E2EPipelineContext { async fn new(account_id: String) -> Result { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let db_pool = PgPool::connect(&database_url).await?; let ensemble = Arc::new(EnsembleCoordinator::new()); ensemble.register_model("DQN".to_string(), 0.25).await?; ensemble.register_model("PPO".to_string(), 0.25).await?; ensemble.register_model("MAMBA2".to_string(), 0.25).await?; ensemble.register_model("TFT".to_string(), 0.25).await?; let audit_logger = EnsembleAuditLogger::new(db_pool.clone()); let risk_limits = RiskLimits::default(); Ok(Self { db_pool, ensemble, audit_logger, risk_limits, account_id, }) } async fn cleanup(&self) -> Result<()> { sqlx::query("DELETE FROM ensemble_predictions WHERE account_id = $1") .bind(&self.account_id) .execute(&self.db_pool) .await?; sqlx::query("DELETE FROM orders WHERE account_id = $1") .bind(&self.account_id) .execute(&self.db_pool) .await?; sqlx::query("DELETE FROM executions WHERE account_id = $1") .bind(&self.account_id) .execute(&self.db_pool) .await?; sqlx::query("DELETE FROM positions WHERE account_id = $1") .bind(&self.account_id) .execute(&self.db_pool) .await?; Ok(()) } fn generate_market_data(&self, num_bars: usize) -> Vec<(f64, f64, f64, f64, f64)> { let mut data = Vec::new(); let mut base_price = 4500.0; for i in 0..num_bars { let trend = (i as f64 * 0.1).sin() * 20.0; let open = base_price + trend; let high = open + (i as f64 % 5.0) + 8.0; let low = open - (i as f64 % 3.0) - 5.0; let close = open + trend * 0.5; let volume = 10000.0 + (i as f64 * 50.0); data.push((open, high, low, close, volume)); base_price = close; } data } async fn run_pipeline(&self, symbol: &str) -> Result { let pipeline_start = Instant::now(); let mut timing = PipelineTiming::default(); // Stage 1: Market Data Ingestion let stage_start = Instant::now(); let market_data = self.generate_market_data(50); timing.data_ingestion = stage_start.elapsed(); // Stage 2: Feature Engineering let stage_start = Instant::now(); let extractor = FeatureExtractor::new(20); let features = extractor.extract(&market_data)?; timing.feature_engineering = stage_start.elapsed(); // Stage 3: Ensemble Prediction let stage_start = Instant::now(); let mut decision = self.ensemble.predict(&features).await?; // Force predictable values for testing decision.action = TradingAction::Buy; decision.confidence = 0.82; timing.ensemble_prediction = stage_start.elapsed(); // Stage 4: Save Prediction let stage_start = Instant::now(); let mut audit = EnsemblePredictionAudit::from_decision(&decision, symbol.to_string()); audit.account_id = Some(self.account_id.clone()); let prediction_id = self.audit_logger.log_prediction(&audit).await?; timing.prediction_save = stage_start.elapsed(); // Stage 5: Risk Validation let stage_start = Instant::now(); let price = 4500.0; let quantity = 10; let risk_result = validate_risk( &self.db_pool, &self.account_id, symbol, quantity, price, decision.confidence, &self.risk_limits, ) .await?; timing.risk_validation = stage_start.elapsed(); if !risk_result.all_passed() { return Ok(PipelineResult { success: false, risk_result: Some(risk_result), timing, ..Default::default() }); } // Stage 6: Order Creation let stage_start = Instant::now(); let order_id = Uuid::new_v4(); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_nanos() as i64; sqlx::query( r#" INSERT INTO orders ( id, client_order_id, symbol, side, order_type, time_in_force, quantity, remaining_quantity, created_at, updated_at, account_id, status ) VALUES ( $1, $2, $3, 'buy', 'market', 'day', $4, $4, $5, $5, $6, 'pending' ) "#, ) .bind(order_id) .bind(format!("e2e_{}", order_id)) .bind(symbol) .bind(quantity) .bind(now) .bind(&self.account_id) .execute(&self.db_pool) .await?; // Link prediction to order sqlx::query("UPDATE ensemble_predictions SET order_id = $1 WHERE id = $2") .bind(order_id) .bind(prediction_id) .execute(&self.db_pool) .await?; timing.order_creation = stage_start.elapsed(); // Stage 7: Order Execution let stage_start = Instant::now(); let fill_price = 4502.0; // 2 bps slippage let execution_id = Uuid::new_v4(); sqlx::query( r#" INSERT INTO executions ( id, order_id, symbol, side, quantity, price, execution_time, account_id ) VALUES ( $1, $2, $3, 'buy', $4, $5, $6, $7 ) "#, ) .bind(execution_id) .bind(order_id) .bind(symbol) .bind(quantity) .bind(fill_price) .bind(now) .bind(&self.account_id) .execute(&self.db_pool) .await?; sqlx::query("UPDATE orders SET status = 'filled', remaining_quantity = 0, updated_at = $2 WHERE id = $1") .bind(order_id) .bind(now) .execute(&self.db_pool) .await?; timing.order_execution = stage_start.elapsed(); // Stage 8: Position Update let stage_start = Instant::now(); sqlx::query( r#" INSERT INTO positions (id, account_id, symbol, quantity, average_price, updated_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (account_id, symbol) DO UPDATE SET quantity = positions.quantity + $4, average_price = ((positions.quantity * positions.average_price) + ($4 * $5)) / (positions.quantity + $4), updated_at = $6 "#, ) .bind(Uuid::new_v4()) .bind(&self.account_id) .bind(symbol) .bind(quantity) .bind(fill_price) .bind(now) .execute(&self.db_pool) .await?; timing.position_update = stage_start.elapsed(); timing.total = pipeline_start.elapsed(); Ok(PipelineResult { success: true, prediction_id: Some(prediction_id), order_id: Some(order_id), execution_id: Some(execution_id), fill_price: Some(fill_price), risk_result: Some(risk_result), timing, }) } } #[derive(Debug, Default)] struct PipelineTiming { data_ingestion: Duration, feature_engineering: Duration, ensemble_prediction: Duration, prediction_save: Duration, risk_validation: Duration, order_creation: Duration, order_execution: Duration, position_update: Duration, total: Duration, } impl PipelineTiming { fn print_summary(&self) { println!(" Pipeline Performance:"); println!(" 1. Data Ingestion: {:?}", self.data_ingestion); println!( " 2. Feature Engineering: {:?}", self.feature_engineering ); println!( " 3. Ensemble Prediction: {:?}", self.ensemble_prediction ); println!(" 4. Prediction Save: {:?}", self.prediction_save); println!(" 5. Risk Validation: {:?}", self.risk_validation); println!(" 6. Order Creation: {:?}", self.order_creation); println!(" 7. Order Execution: {:?}", self.order_execution); println!(" 8. Position Update: {:?}", self.position_update); println!(" ──────────────────────────────────"); println!(" TOTAL E2E LATENCY: {:?}", self.total); } } #[derive(Debug, Default)] struct PipelineResult { success: bool, prediction_id: Option, order_id: Option, execution_id: Option, fill_price: Option, risk_result: Option, timing: PipelineTiming, } // ============================================================================ // E2E Test: Complete ML Pipeline // ============================================================================ #[tokio::test] #[ignore = "Integration test - requires services running"] async fn test_complete_ensemble_risk_execution_pipeline() -> Result<()> { println!("\n{'═'*80}"); println!("E2E TEST: ENSEMBLE → RISK → EXECUTION PIPELINE"); println!("{'═'*80}\n"); let ctx = E2EPipelineContext::new("test_e2e_pipeline".to_string()).await?; ctx.cleanup().await?; // Run complete pipeline println!("Running Complete ML Pipeline...\n"); let result = ctx.run_pipeline("ES.FUT").await?; // Verify success assert!(result.success, "Pipeline should succeed"); // Print risk validation results if let Some(risk_result) = &result.risk_result { risk_result.print_summary(); assert!(risk_result.all_passed(), "All risk checks should pass"); } println!(); // Print performance breakdown result.timing.print_summary(); // Validate performance targets assert!( result.timing.feature_engineering < Duration::from_millis(100), "Feature engineering should be <100ms" ); assert!( result.timing.ensemble_prediction < Duration::from_millis(500), "Ensemble prediction should be <500ms" ); assert!( result.timing.risk_validation < Duration::from_millis(50), "Risk validation should be <50ms" ); assert!( result.timing.order_creation < Duration::from_millis(50), "Order creation should be <50ms" ); assert!( result.timing.order_execution < Duration::from_millis(100), "Order execution should be <100ms" ); assert!( result.timing.position_update < Duration::from_millis(50), "Position update should be <50ms" ); assert!( result.timing.total < Duration::from_secs(3), "Total E2E should be <3 seconds" ); // Verify database records let prediction_id = result.prediction_id.expect("Should have prediction_id"); let order_id = result.order_id.expect("Should have order_id"); // Verify prediction linked to order let linked = sqlx::query("SELECT order_id FROM ensemble_predictions WHERE id = $1") .bind(prediction_id) .fetch_one(&ctx.db_pool) .await?; let linked_order_id: Option = linked.get("order_id"); assert_eq!( linked_order_id, Some(order_id), "Prediction should link to order" ); // Verify position let position = sqlx::query( r#" SELECT quantity, average_price FROM positions WHERE account_id = $1 AND symbol = 'ES.FUT' "#, ) .bind(&ctx.account_id) .fetch_one(&ctx.db_pool) .await?; let qty: i32 = position.get("quantity"); let avg_price: f64 = position.get("average_price"); assert_eq!(qty, 10, "Position quantity should be 10"); assert!( (avg_price - 4502.0).abs() < 1.0, "Position avg price should be ~4502" ); println!("\n{'═'*80}"); println!("✅ E2E PIPELINE TEST PASSED"); println!("{'═'*80}"); println!( "Total Latency: {:?} (target: <2 seconds)", result.timing.total ); println!("All Risk Checks: PASSED"); println!("Database Records: VERIFIED"); println!("{'═'*80}\n"); Ok(()) }