- 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>
715 lines
24 KiB
Rust
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);
|
|
}
|
|
}
|