Files
foxhunt/crates/ml-stress-testing/src/lib.rs
jgrusewski d06c99b0e1 fix(clippy): achieve zero warnings across entire workspace
- Fix format_push_string: write!() instead of push_str(&format!()) (25 sites)
- Fix str_to_string: .to_owned() instead of .to_string() on &str (6 sites)
- Fix unseparated_literal_suffix: add _ separator (6 sites)
- Fix multiple_inherent_impl: merge split impl blocks in TGGN, TFT, OFI (3)
- Fix else_if_without_else: add exhaustive else clauses (3 sites)
- Fix if_then_some_else_none: use .then().transpose() (1 site)
- Fix unwrap_in_result: replace expect() with match + ? (2 sites)
- Fix wildcard_enum_match_arm: enumerate Storage variants explicitly (2)
- Fix decimal_literal_representation: use hex for power-of-2 constants (5)
- Fix rc_buffer: Arc<Vec<T>> → Arc<[T]> for OFI features
- Fix needless_range_loop: convert to iterator patterns (17 sites)
- Fix used_underscore_binding: remove prefix on used vars (6 sites)
- Fix doc list item indentation (7 sites)
- Allow too_many_arguments on ML training functions (4)
- Allow multiple_unsafe_ops_per_block on CUDA FFI functions (3)
- Allow upper_case_acronyms on SLSTM/MLSTM model names (2)
- Add ML-crate pedantic allows: shadow, similar_names, type_complexity,
  indexing_slicing, partial_pub_fields, non_ascii_literal, same_name_method
  (following existing ml-labeling/ml-universe pattern)

Result: cargo clippy --workspace -- -D warnings passes with zero warnings.
All 2758+ lib tests pass (2 pre-existing backtesting failures unchanged).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:18:57 +01:00

715 lines
24 KiB
Rust

#![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<TestPhase>,
/// Models to test
pub models_to_test: Vec<String>,
/// Market conditions to simulate
pub market_conditions: Vec<MarketCondition>,
/// 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<Self> {
// 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<Self> {
let symbols: Vec<String> = 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<Self> {
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<std::sync::Arc<dyn MLModel>>,
) -> Result<StressTestReport> {
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<dyn MLModel>],
market_rx: &mut mpsc::Receiver<MarketDataUpdate>,
prediction_tx: &mpsc::Sender<PredictionResult>,
) -> Result<PhaseResult> {
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<Features> {
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<PhaseResult>,
total_duration: Duration,
) -> Result<StressTestReport> {
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::<u64>() 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<String> {
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<String>,
}
/// Phase execution statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseStats {
pub successful_predictions: u64,
pub failed_predictions: u64,
pub latencies: Vec<u64>,
}
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<String>,
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);
}
}