#![deny(clippy::unwrap_used, clippy::expect_used)] #![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))] #![allow(clippy::module_name_repetitions)] #![allow(clippy::integer_division)] #![allow(dead_code)] #![allow(missing_docs)] #![allow(missing_debug_implementations)] #![allow(clippy::float_arithmetic)] #![allow(clippy::doc_markdown)] #![allow(clippy::indexing_slicing)] #![allow(clippy::missing_const_for_fn)] #![allow(clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated)] // Tensor ops: let x = x.relu() is idiomatic #![allow(clippy::non_ascii_literal)] // Math symbols in ML documentation and error messages #![allow(clippy::partial_pub_fields)] // ML config structs: some fields are pub API, some internal #![allow(clippy::same_name_method)] // Intentional: inherent methods shadow trait defaults for ML-specific behavior //! Stress testing framework for ML models under high-volume market data conditions //! //! This crate provides comprehensive stress testing capabilities to validate ML model //! performance under realistic HFT market conditions with high-frequency data feeds. // Re-export key types from ml-core that are used throughout this crate pub use common::model_types::ModelType; pub use ml_core::{Features, MLModel, ModelPrediction}; pub mod load_generator; pub mod market_simulator; pub mod performance_analyzer; // Re-export key types that are needed by external users pub use load_generator::{LoadGenerator, LoadProfile, TrafficPattern}; pub use market_simulator::{MarketCondition, MarketDataSimulator, SimulatorConfig}; pub use performance_analyzer::{LatencyStats, PerformanceAnalyzer, StressTestReport}; use common::types::Price; use rust_decimal::Decimal; use anyhow::Result; use rust_decimal::prelude::ToPrimitive; use serde::{Deserialize, Serialize}; use std::time::{Duration, Instant}; use tokio::sync::mpsc; /// Comprehensive stress test configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StressTestConfig { /// Test duration in seconds pub duration_seconds: u64, /// Target requests per second pub target_rps: u32, /// Number of concurrent connections pub concurrent_connections: u32, /// Market data feed rate (updates per second) pub market_data_rate: u32, /// Test phases with different load patterns pub test_phases: Vec, /// Models to test pub models_to_test: Vec, /// Market conditions to simulate pub market_conditions: Vec, /// Performance requirements pub requirements: PerformanceRequirements, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestPhase { pub name: String, pub duration_seconds: u64, pub load_multiplier: f64, pub market_volatility: f64, pub error_injection_rate: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PerformanceRequirements { /// Maximum acceptable latency in microseconds pub max_latency_us: u64, /// 95th percentile latency requirement pub p95_latency_us: u64, /// 99th percentile latency requirement pub p99_latency_us: u64, /// Maximum acceptable error rate pub max_error_rate: f64, /// Minimum throughput (predictions per second) pub min_throughput: u32, /// Maximum memory usage in MB pub max_memory_mb: u64, /// Maximum CPU utilization percentage pub max_cpu_percent: f64, } impl Default for PerformanceRequirements { fn default() -> Self { Self { max_latency_us: 100, // 100us max latency p95_latency_us: 50, // 50us 95th percentile p99_latency_us: 80, // 80us 99th percentile max_error_rate: 0.01, // 1% max error rate min_throughput: 10000, // 10k predictions/sec max_memory_mb: 1024, // 1GB max memory max_cpu_percent: 80.0, // 80% max CPU } } } /// Main stress testing orchestrator #[derive(Debug)] pub struct StressTestOrchestrator { config: StressTestConfig, market_simulator: MarketDataSimulator, load_generator: LoadGenerator, performance_analyzer: PerformanceAnalyzer, } impl StressTestOrchestrator { /// Create new stress test orchestrator with configuration-driven market simulation pub fn new(config: StressTestConfig) -> Result { // Use configuration-driven approach - load from config crate let simulation_config = config::SimulationConfig::default(); // Use test fixtures for symbol configuration let test_symbols = vec!["TEST_LARGE_1", "TEST_LARGE_2", "TEST_MID_1", "TEST_SMALL_1"]; let simulator_config = SimulatorConfig { symbols: test_symbols.into_iter().map(|s| s.to_owned()).collect(), update_rate_hz: config.market_data_rate, volatility: 0.02, trend: 0.0, simulation_config: Some(simulation_config), }; let market_simulator = MarketDataSimulator::new(simulator_config)?; let load_generator = LoadGenerator::new(config.target_rps, config.concurrent_connections)?; let performance_analyzer = PerformanceAnalyzer::new(); Ok(Self { config, market_simulator, load_generator, performance_analyzer, }) } /// Create stress test orchestrator with custom simulation configuration pub fn new_with_simulation_config( config: StressTestConfig, simulation_config: config::SimulationConfig, ) -> Result { let symbols: Vec = simulation_config .initial_market_state .symbols .keys() .cloned() .collect(); let simulator_config = SimulatorConfig { symbols, update_rate_hz: config.market_data_rate, volatility: simulation_config.parameters.base_volatility, trend: simulation_config.parameters.trend, simulation_config: Some(simulation_config), }; let market_simulator = MarketDataSimulator::new(simulator_config)?; let load_generator = LoadGenerator::new(config.target_rps, config.concurrent_connections)?; let performance_analyzer = PerformanceAnalyzer::new(); Ok(Self { config, market_simulator, load_generator, performance_analyzer, }) } /// Create stress test orchestrator for testing with generated test symbols pub fn new_for_testing(config: StressTestConfig) -> Result { let simulation_config = config::SimulationConfig::default(); let test_symbols = MarketDataSimulator::generate_test_symbols(&simulation_config); let simulator_config = SimulatorConfig { symbols: test_symbols, update_rate_hz: config.market_data_rate, volatility: simulation_config.parameters.base_volatility, trend: simulation_config.parameters.trend, simulation_config: Some(simulation_config), }; let market_simulator = MarketDataSimulator::new(simulator_config)?; let load_generator = LoadGenerator::new(config.target_rps, config.concurrent_connections)?; let performance_analyzer = PerformanceAnalyzer::new(); Ok(Self { config, market_simulator, load_generator, performance_analyzer, }) } /// Run comprehensive stress test pub async fn run_stress_test( &mut self, models: Vec>, ) -> Result { tracing::info!("Starting stress test with {} models", models.len()); // Create channels for communication let (market_tx, mut market_rx) = mpsc::channel(10000); let (prediction_tx, _prediction_rx) = mpsc::channel(10000); // Start market data simulation let simulator_handle = { let mut simulator = self.market_simulator.clone(); tokio::spawn(async move { simulator.start_simulation(market_tx).await }) }; // Start performance monitoring let analyzer = self.performance_analyzer.clone(); let monitor_handle = tokio::spawn(async move { analyzer.start_monitoring().await }); // Execute test phases let mut phase_results = Vec::new(); let test_start = Instant::now(); // Clone the test phases to avoid borrowing conflicts let test_phases = self.config.test_phases.clone(); for (phase_idx, phase) in test_phases.into_iter().enumerate() { tracing::info!("Starting test phase {}: {}", phase_idx + 1, phase.name); let phase_result = self .run_test_phase(&phase, &models, &mut market_rx, &prediction_tx) .await?; phase_results.push(phase_result); } // Stop simulation and monitoring simulator_handle.abort(); monitor_handle.abort(); // Generate comprehensive report let total_duration = test_start.elapsed(); let report = self .generate_stress_test_report(phase_results, total_duration) .await?; tracing::info!( "Stress test completed in {:.2}s", total_duration.as_secs_f64() ); Ok(report) } /// Run individual test phase async fn run_test_phase( &mut self, phase: &TestPhase, models: &[std::sync::Arc], market_rx: &mut mpsc::Receiver, prediction_tx: &mpsc::Sender, ) -> Result { let phase_start = Instant::now(); let phase_duration = Duration::from_secs(phase.duration_seconds); let mut phase_stats = PhaseStats::new(); // Adjust load generator for this phase self.load_generator .set_load_multiplier(phase.load_multiplier); while phase_start.elapsed() < phase_duration { // Process market data updates while let Ok(market_update) = market_rx.try_recv() { // Convert market data to features let features = self.convert_market_data_to_features(&market_update)?; // Run predictions on all models for model in models { let model_start = Instant::now(); match model.predict(&features).await { Ok(prediction) => { let latency_us = model_start.elapsed().as_micros() as u64; phase_stats.record_successful_prediction(latency_us); let result = PredictionResult { model_name: model.name().to_owned(), model_type: model.model_type(), prediction, latency_us, timestamp: std::time::SystemTime::now(), success: true, error_message: None, }; drop(prediction_tx.send(result).await); }, Err(e) => { let latency_us = model_start.elapsed().as_micros() as u64; phase_stats.record_failed_prediction(latency_us); let result = PredictionResult { model_name: model.name().to_owned(), model_type: model.model_type(), prediction: ModelPrediction::new( model.name().to_owned(), 0.0, 0.0, ), latency_us, timestamp: std::time::SystemTime::now(), success: false, error_message: Some(e.to_string()), }; drop(prediction_tx.send(result).await); }, } } } // Small delay to prevent busy waiting tokio::time::sleep(Duration::from_micros(100)).await; } Ok(PhaseResult { phase_name: phase.name.clone(), duration: phase_start.elapsed(), stats: phase_stats, }) } /// Convert market data to ML features #[allow(clippy::unnecessary_wraps)] fn convert_market_data_to_features(&self, market_data: &MarketDataUpdate) -> Result { let values = vec![ market_data.price.to_f64(), market_data.volume.to_f64().unwrap_or(0.0), market_data.bid.to_f64(), market_data.ask.to_f64(), market_data.spread().to_f64(), market_data.mid_price().to_f64(), ]; let names = vec![ "price".to_owned(), "volume".to_owned(), "bid".to_owned(), "ask".to_owned(), "spread".to_owned(), "mid_price".to_owned(), ]; Ok(Features::new(values, names).with_symbol(market_data.symbol.clone())) } /// Generate comprehensive stress test report async fn generate_stress_test_report( &self, phase_results: Vec, total_duration: Duration, ) -> Result { let mut total_predictions = 0; let mut total_errors = 0; let mut all_latencies = Vec::new(); for phase in &phase_results { total_predictions += phase.stats.successful_predictions + phase.stats.failed_predictions; total_errors += phase.stats.failed_predictions; all_latencies.extend(&phase.stats.latencies); } let error_rate = if total_predictions > 0 { total_errors as f64 / total_predictions as f64 } else { 0.0 }; let latency_stats = self.calculate_latency_statistics(&all_latencies); // Check if requirements are met let requirements_met = self.check_requirements(&latency_stats, error_rate); let recommendations = self.generate_recommendations(&latency_stats, error_rate); Ok(StressTestReport { config: self.config.clone(), total_duration, phase_results, total_predictions, total_errors, error_rate, latency_stats, requirements_met, throughput_achieved: total_predictions as f64 / total_duration.as_secs_f64(), recommendations, }) } fn calculate_latency_statistics(&self, latencies: &[u64]) -> LatencyStats { if latencies.is_empty() { return LatencyStats::default(); } let mut sorted_latencies = latencies.to_vec(); sorted_latencies.sort_unstable(); let len = sorted_latencies.len(); let mean = sorted_latencies.iter().sum::() as f64 / len as f64; let min = sorted_latencies[0]; let max = sorted_latencies[len - 1]; let p50 = sorted_latencies[len * 50 / 100]; let p95 = sorted_latencies[len * 95 / 100]; let p99 = sorted_latencies[len * 99 / 100]; LatencyStats { mean, min, max, p50, p95, p99, count: len as u64, } } fn check_requirements( &self, latency_stats: &LatencyStats, error_rate: f64, ) -> RequirementsCheck { RequirementsCheck { latency_ok: latency_stats.max <= self.config.requirements.max_latency_us, p95_latency_ok: latency_stats.p95 <= self.config.requirements.p95_latency_us, p99_latency_ok: latency_stats.p99 <= self.config.requirements.p99_latency_us, error_rate_ok: error_rate <= self.config.requirements.max_error_rate, overall_pass: latency_stats.max <= self.config.requirements.max_latency_us && latency_stats.p95 <= self.config.requirements.p95_latency_us && latency_stats.p99 <= self.config.requirements.p99_latency_us && error_rate <= self.config.requirements.max_error_rate, } } fn generate_recommendations( &self, latency_stats: &LatencyStats, error_rate: f64, ) -> Vec { let mut recommendations = Vec::new(); if latency_stats.p99 > self.config.requirements.p99_latency_us { recommendations.push(format!( "P99 latency ({}us) exceeds requirement ({}us). Consider model optimization or hardware upgrades.", latency_stats.p99, self.config.requirements.p99_latency_us )); } if error_rate > self.config.requirements.max_error_rate { recommendations.push(format!( "Error rate ({:.2}%) exceeds requirement ({:.2}%). Investigate model reliability.", error_rate * 100.0, self.config.requirements.max_error_rate * 100.0 )); } if latency_stats.mean > 50.0 { recommendations .push("Consider enabling GPU acceleration for better performance.".to_owned()); } if recommendations.is_empty() { recommendations .push("All performance requirements met. System ready for production.".to_owned()); } recommendations } } /// Market data update structure #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MarketDataUpdate { pub symbol: String, pub price: Price, pub volume: Decimal, pub bid: Price, pub ask: Price, pub timestamp: std::time::SystemTime, } impl MarketDataUpdate { pub fn spread(&self) -> Price { Price::from_f64(self.ask.to_f64() - self.bid.to_f64()).unwrap_or_default() } pub fn mid_price(&self) -> Price { Price::from_f64((self.bid.to_f64() + self.ask.to_f64()) / 2.0).unwrap_or_default() } } /// Prediction result with timing information #[derive(Debug, Clone)] pub struct PredictionResult { pub model_name: String, pub model_type: ModelType, pub prediction: ModelPrediction, pub latency_us: u64, pub timestamp: std::time::SystemTime, pub success: bool, pub error_message: Option, } /// Phase execution statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PhaseStats { pub successful_predictions: u64, pub failed_predictions: u64, pub latencies: Vec, } impl Default for PhaseStats { fn default() -> Self { Self::new() } } impl PhaseStats { pub fn new() -> Self { Self { successful_predictions: 0, failed_predictions: 0, latencies: Vec::new(), } } pub fn record_successful_prediction(&mut self, latency_us: u64) { self.successful_predictions += 1; self.latencies.push(latency_us); } pub fn record_failed_prediction(&mut self, latency_us: u64) { self.failed_predictions += 1; self.latencies.push(latency_us); } } /// Phase execution result #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PhaseResult { pub phase_name: String, pub duration: Duration, pub stats: PhaseStats, } /// Requirements check result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RequirementsCheck { pub latency_ok: bool, pub p95_latency_ok: bool, pub p99_latency_ok: bool, pub error_rate_ok: bool, pub overall_pass: bool, } /// Create default HFT stress test configuration with production-ready settings pub fn create_hft_stress_test_config() -> StressTestConfig { StressTestConfig { duration_seconds: 300, // 5 minutes target_rps: 50000, // 50k requests per second concurrent_connections: 100, market_data_rate: 10000, // 10k market updates per second test_phases: vec![ TestPhase { name: "warmup".to_owned(), duration_seconds: 60, load_multiplier: 0.5, market_volatility: 0.01, error_injection_rate: 0.0, }, TestPhase { name: "normal_load".to_owned(), duration_seconds: 120, load_multiplier: 1.0, market_volatility: 0.02, error_injection_rate: 0.001, }, TestPhase { name: "peak_load".to_owned(), duration_seconds: 60, load_multiplier: 2.0, market_volatility: 0.05, error_injection_rate: 0.005, }, TestPhase { name: "stress_load".to_owned(), duration_seconds: 60, load_multiplier: 5.0, market_volatility: 0.1, error_injection_rate: 0.01, }, ], models_to_test: vec![ "TLOB_Transformer".to_owned(), "MAMBA_SSM".to_owned(), "DQN_Agent".to_owned(), ], market_conditions: vec![ MarketCondition::Normal, MarketCondition::HighVolatility, MarketCondition::Flash, ], requirements: PerformanceRequirements::default(), } } /// Create stress test configuration optimized for testing with generic symbols pub fn create_test_stress_test_config() -> StressTestConfig { let mut config = create_hft_stress_test_config(); // Reduce intensity for testing config.duration_seconds = 60; // 1 minute for testing config.target_rps = 1000; // 1k requests per second for testing config.market_data_rate = 100; // 100 updates per second for testing // Simplified test phases config.test_phases = vec![TestPhase { name: "test_phase".to_owned(), duration_seconds: 60, load_multiplier: 1.0, market_volatility: 0.02, error_injection_rate: 0.0, }]; config } /// Create stress test configuration with custom simulation parameters pub fn create_custom_stress_test_config( _symbols: Vec, simulation_config: config::SimulationConfig, ) -> StressTestConfig { let mut config = create_hft_stress_test_config(); // Use simulation config parameters config.market_data_rate = simulation_config.parameters.update_rate_hz; // Update volatility in test phases based on simulation config for phase in &mut config.test_phases { phase.market_volatility = simulation_config.parameters.base_volatility; } config } #[cfg(test)] mod tests { use super::*; #[test] fn test_stress_test_config_creation() { let config = create_hft_stress_test_config(); assert_eq!(config.test_phases.len(), 4); assert_eq!(config.target_rps, 50000); } #[test] fn test_market_data_calculations() { let update = MarketDataUpdate { symbol: "TEST_LARGE_1".to_owned(), price: Price::from_f64(150.0).unwrap(), volume: Decimal::try_from(1000.0).unwrap(), bid: Price::from_f64(149.95).unwrap(), ask: Price::from_f64(150.05).unwrap(), timestamp: std::time::SystemTime::now(), }; assert!((update.spread().to_f64() - 0.10).abs() < 0.001); assert!((update.mid_price().to_f64() - 150.0).abs() < 0.001); } #[test] fn test_configuration_driven_simulator() { let simulation_config = config::SimulationConfig::default(); let test_symbols = MarketDataSimulator::generate_test_symbols(&simulation_config); assert_eq!(test_symbols.len(), simulation_config.test_symbols.count); assert!(test_symbols[0].starts_with(&simulation_config.test_symbols.symbol_prefix)); } #[test] fn test_custom_stress_test_config() { let symbols = vec!["TEST001".to_owned(), "TEST002".to_owned()]; let simulation_config = config::SimulationConfig::default(); let stress_config = create_custom_stress_test_config(symbols, simulation_config.clone()); assert_eq!( stress_config.market_data_rate, simulation_config.parameters.update_rate_hz ); } #[test] fn test_phase_stats() { let mut stats = PhaseStats::new(); stats.record_successful_prediction(25); stats.record_successful_prediction(30); stats.record_failed_prediction(100); assert_eq!(stats.successful_predictions, 2); assert_eq!(stats.failed_predictions, 1); assert_eq!(stats.latencies.len(), 3); } }