diff --git a/ml/src/benchmark/performance_tracker.rs b/ml/src/benchmark/performance_tracker.rs index 5fc0bfb42..956b65a08 100644 --- a/ml/src/benchmark/performance_tracker.rs +++ b/ml/src/benchmark/performance_tracker.rs @@ -426,14 +426,14 @@ impl PerformanceTracker { if result.has_regression { report.push_str("## ❌ Regression Detected\n\n"); - let _ = writeln!(report, "{}\n", result.summary); + _ = writeln!(report, "{}\n", result.summary); report.push_str("### Regressions\n\n"); report.push_str("| Metric | Baseline | Current | Change |\n"); report.push_str("|--------|----------|---------|--------|\n"); for regression in &result.regressions { - let _ = writeln!( + _ = writeln!( report, "| {} | {:.2} | {:.2} | {:+.1}% |", regression.metric, @@ -445,11 +445,11 @@ impl PerformanceTracker { report.push_str("\n### Details\n\n"); for regression in &result.regressions { - let _ = writeln!(report, "- {}", regression.description); + _ = writeln!(report, "- {}", regression.description); } } else { report.push_str("## ✅ No Regression\n\n"); - let _ = writeln!(report, "{}\n", result.summary); + _ = writeln!(report, "{}\n", result.summary); report.push_str("### Metrics\n\n"); report.push_str("| Metric | Baseline | Current | Change |\n"); @@ -495,7 +495,7 @@ impl PerformanceTracker { 0.0 }; - let _ = writeln!( + _ = writeln!( report, "| {} | {:.2} | {:.2} | {:+.1}% |", name, baseline, current, percent_change @@ -503,13 +503,13 @@ impl PerformanceTracker { } } - let _ = writeln!( + _ = writeln!( report, "\n**Baseline**: {} (commit: {})", result.baseline.timestamp.format("%Y-%m-%d %H:%M:%S"), result.baseline.git_commit ); - let _ = writeln!( + _ = writeln!( report, "**Current**: {} (commit: {})", result.current.timestamp.format("%Y-%m-%d %H:%M:%S"), diff --git a/ml/src/benchmarks.rs b/ml/src/benchmarks.rs index 91fe69496..d4d6f03ad 100644 --- a/ml/src/benchmarks.rs +++ b/ml/src/benchmarks.rs @@ -587,46 +587,46 @@ impl MLBenchmarkRunner { // System Information report.push_str("## System Information\n"); - let _ = writeln!(report, "- OS: {}", suite.system_info.os); - let _ = writeln!( + _ = writeln!(report, "- OS: {}", suite.system_info.os); + _ = writeln!( report, "- Architecture: {}", suite.system_info.architecture ); - let _ = writeln!(report, "- CPU Cores: {}", suite.system_info.cpu_count); - let _ = writeln!( + _ = writeln!(report, "- CPU Cores: {}", suite.system_info.cpu_count); + _ = writeln!( report, "- Available Memory: {:.1} GB", suite.system_info.available_memory_gb ); - let _ = writeln!( + _ = writeln!( report, "- Disk Space: {:.1} GB total, {:.1} GB available", suite.system_info.disk_total_gb, suite.system_info.disk_available_gb ); if let Some(temp) = suite.system_info.cpu_temperature_celsius { - let _ = writeln!(report, "- CPU Temperature: {:.1}°C", temp); + _ = writeln!(report, "- CPU Temperature: {:.1}°C", temp); } if let Some(gpu) = &suite.gpu_info { - let _ = writeln!(report, "- GPU: {}", gpu.name); - let _ = writeln!(report, "- GPU Memory: {:.1} GB", gpu.memory_gb); - let _ = writeln!( + _ = writeln!(report, "- GPU: {}", gpu.name); + _ = writeln!(report, "- GPU Memory: {:.1} GB", gpu.memory_gb); + _ = writeln!( report, "- Compute Capability: {}", gpu.compute_capability ); } - let _ = writeln!(report, "\n## Benchmark Configuration"); - let _ = writeln!( + _ = writeln!(report, "\n## Benchmark Configuration"); + _ = writeln!( report, "- Target Latency: {}μs", self.config.target_latency_us ); - let _ = writeln!(report, "- Test Runs: {}", self.config.test_runs); - let _ = writeln!(report, "- Warmup Runs: {}", self.config.warmup_runs); - let _ = writeln!(report, "- Batch Size: {}", self.config.batch_size); + _ = writeln!(report, "- Test Runs: {}", self.config.test_runs); + _ = writeln!(report, "- Warmup Runs: {}", self.config.warmup_runs); + _ = writeln!(report, "- Batch Size: {}", self.config.batch_size); report.push_str("\n## Performance Results\n\n"); report.push_str("| Model | Device | Avg (μs) | P95 (μs) | P99 (μs) | Max (μs) | Throughput (pps) | Target Met |\n"); @@ -634,7 +634,7 @@ impl MLBenchmarkRunner { for result in &suite.results { let target_met = if result.target_met { "✅" } else { "❌" }; - let _ = writeln!( + _ = writeln!( report, "| {} | {} | {:.1} | {:.1} | {:.1} | {:.1} | {:.0} | {} |", result.model_name, @@ -649,7 +649,7 @@ impl MLBenchmarkRunner { } let models_meeting_target = suite.results.iter().filter(|r| r.target_met).count(); - let _ = writeln!( + _ = writeln!( report, "\n**Summary**: {}/{} models meet the <{}μs latency target", models_meeting_target, @@ -657,7 +657,7 @@ impl MLBenchmarkRunner { self.config.target_latency_us ); - let _ = writeln!( + _ = writeln!( report, "\nTotal benchmark time: {:.2}ms", suite.total_duration_ms diff --git a/ml/src/data_loaders/calibration.rs b/ml/src/data_loaders/calibration.rs index ea6f8d4bc..493564368 100644 --- a/ml/src/data_loaders/calibration.rs +++ b/ml/src/data_loaders/calibration.rs @@ -125,7 +125,9 @@ pub async fn generate_calibration_dataset>( let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?; // Copy DBN file to temp directory (DbnSequenceLoader expects a directory) - let temp_file = temp_dir.path().join(path.file_name().unwrap()); + let temp_file = temp_dir.path().join(path.file_name().ok_or_else(|| { + anyhow::anyhow!("DBN path has no file name: {:?}", path) + })?); std::fs::copy(path, &temp_file) .with_context(|| format!("Failed to copy DBN file to {:?}", temp_file))?; diff --git a/ml/src/data_loaders/dbn_sequence_loader.rs b/ml/src/data_loaders/dbn_sequence_loader.rs index 044fb0c80..fd9ca5b0c 100644 --- a/ml/src/data_loaders/dbn_sequence_loader.rs +++ b/ml/src/data_loaders/dbn_sequence_loader.rs @@ -810,13 +810,13 @@ impl DbnSequenceLoader { .map(|bar| ProcessedMessage::Ohlcv { symbol: symbol.clone(), open: common::Price::from_f64(bar.open) - .unwrap_or_else(|_| common::Price::from_f64(0.0).unwrap()), + .unwrap_or_default(), high: common::Price::from_f64(bar.high) - .unwrap_or_else(|_| common::Price::from_f64(0.0).unwrap()), + .unwrap_or_default(), low: common::Price::from_f64(bar.low) - .unwrap_or_else(|_| common::Price::from_f64(0.0).unwrap()), + .unwrap_or_default(), close: common::Price::from_f64(bar.close) - .unwrap_or_else(|_| common::Price::from_f64(0.0).unwrap()), + .unwrap_or_default(), volume: Decimal::from_f64(bar.volume).unwrap_or(Decimal::ZERO), timestamp: trading_engine::timing::HardwareTimestamp::from_nanos( bar.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64, diff --git a/ml/src/data_loaders/streaming_dbn_loader.rs b/ml/src/data_loaders/streaming_dbn_loader.rs index ae175987a..024cde4aa 100644 --- a/ml/src/data_loaders/streaming_dbn_loader.rs +++ b/ml/src/data_loaders/streaming_dbn_loader.rs @@ -553,7 +553,10 @@ impl SequenceStream { return Ok(false); } - let file_path = self.dbn_files.pop_front().unwrap(); + let file_path = match self.dbn_files.pop_front() { + Some(path) => path, + None => return Ok(false), + }; info!( "📖 Loading file: {:?}", file_path.file_name().unwrap_or_default() diff --git a/ml/src/data_validation/validator.rs b/ml/src/data_validation/validator.rs index 78127eee1..a1a19ebc2 100644 --- a/ml/src/data_validation/validator.rs +++ b/ml/src/data_validation/validator.rs @@ -58,7 +58,7 @@ impl ValidationResult { pub fn error_summary(&self) -> String { let mut summary = String::new(); for error in &self.errors { - let _ = writeln!(summary, "{}: {}", error.category, error.message); + _ = writeln!(summary, "{}: {}", error.category, error.message); } summary } @@ -78,9 +78,9 @@ impl ValidationResult { report.push_str("❌ Status: FAIL\n"); } - let _ = writeln!(report, "📊 Total bars validated: {}", self.total_bars); - let _ = writeln!(report, "🔴 Errors: {}", self.error_count()); - let _ = writeln!(report, "🟡 Warnings: {}\n", self.warning_count()); + _ = writeln!(report, "📊 Total bars validated: {}", self.total_bars); + _ = writeln!(report, "🔴 Errors: {}", self.error_count()); + _ = writeln!(report, "🟡 Warnings: {}\n", self.warning_count()); // Errors section if !self.errors.is_empty() { @@ -97,17 +97,17 @@ impl ValidationResult { } for (category, errors) in categories { - let _ = writeln!(report, "\n {} ({} errors):", category, errors.len()); + _ = writeln!(report, "\n {} ({} errors):", category, errors.len()); for error in errors.iter().take(5) { // Show first 5 errors per category if let Some(idx) = error.bar_index { - let _ = writeln!(report, " [Bar {}] {}", idx, error.message); + _ = writeln!(report, " [Bar {}] {}", idx, error.message); } else { - let _ = writeln!(report, " {}", error.message); + _ = writeln!(report, " {}", error.message); } } if errors.len() > 5 { - let _ = writeln!(report, " ... and {} more", errors.len() - 5); + _ = writeln!(report, " ... and {} more", errors.len() - 5); } } report.push_str("\n"); @@ -128,7 +128,7 @@ impl ValidationResult { } for (category, warnings) in categories { - let _ = writeln!( + _ = writeln!( report, "\n {} ({} warnings):", category, @@ -137,13 +137,13 @@ impl ValidationResult { for warning in warnings.iter().take(3) { // Show first 3 warnings per category if let Some(idx) = warning.bar_index { - let _ = writeln!(report, " [Bar {}] {}", idx, warning.message); + _ = writeln!(report, " [Bar {}] {}", idx, warning.message); } else { - let _ = writeln!(report, " {}", warning.message); + _ = writeln!(report, " {}", warning.message); } } if warnings.len() > 3 { - let _ = writeln!(report, " ... and {} more", warnings.len() - 3); + _ = writeln!(report, " ... and {} more", warnings.len() - 3); } } report.push_str("\n"); diff --git a/ml/src/dqn/action_space.rs b/ml/src/dqn/action_space.rs index 47fb6a56d..fa87ab289 100644 --- a/ml/src/dqn/action_space.rs +++ b/ml/src/dqn/action_space.rs @@ -320,7 +320,13 @@ pub fn get_valid_action_mask(_current_position: f64, max_position: f64) -> Vec a, + Err(_) => { + mask[idx] = false; + continue; + } + }; // Get target exposure from this action let target_exposure = action.target_exposure(); diff --git a/ml/src/dqn/multi_asset.rs b/ml/src/dqn/multi_asset.rs index 9b3a7f13b..30995f9bb 100644 --- a/ml/src/dqn/multi_asset.rs +++ b/ml/src/dqn/multi_asset.rs @@ -103,8 +103,12 @@ impl MultiAssetPortfolioTracker { let positions = symbols .iter() .map(|sym| { + let capital = initial_capital_per_symbol + .to_string() + .parse::() + .unwrap_or(100_000.0); let tracker = PortfolioTracker::new( - initial_capital_per_symbol.to_string().parse::().unwrap(), + capital, 0.0001, // Default spread 0.0, // No cash reserve by default ); @@ -140,8 +144,12 @@ impl MultiAssetPortfolioTracker { let positions = symbols .iter() .map(|sym| { + let capital = initial_capital_per_symbol + .to_string() + .parse::() + .unwrap_or(100_000.0); let tracker = PortfolioTracker::new( - initial_capital_per_symbol.to_string().parse::().unwrap(), + capital, 0.0001, cash_reserve_percent, ); diff --git a/ml/src/dqn/nstep_buffer.rs b/ml/src/dqn/nstep_buffer.rs index 0bccc96aa..0adde46e5 100644 --- a/ml/src/dqn/nstep_buffer.rs +++ b/ml/src/dqn/nstep_buffer.rs @@ -160,8 +160,7 @@ impl NStepBuffer { pub fn flush(&mut self) -> Vec { let mut result = Vec::new(); - while !self.buffer.is_empty() { - let first = self.buffer.pop_front().unwrap(); + while let Some(first) = self.buffer.pop_front() { // Compute truncated n-step return (fewer than n steps) let mut n_step_reward_f64 = first.reward as f64; diff --git a/ml/src/dqn/performance_tests.rs b/ml/src/dqn/performance_tests.rs index 58c741c53..25bea4d14 100644 --- a/ml/src/dqn/performance_tests.rs +++ b/ml/src/dqn/performance_tests.rs @@ -103,7 +103,7 @@ impl RainbowPerformanceValidator { for (name, result) in results { let status = if result.meets_target { "✅" } else { "❌" }; - let _ = writeln!( + _ = writeln!( report, "{} {}: {:.1}μs avg (target: {}μs)", status, name, result.mean_latency_us, self.config.max_latency_us diff --git a/ml/src/dqn/reward.rs b/ml/src/dqn/reward.rs index d369b9ded..93756505d 100644 --- a/ml/src/dqn/reward.rs +++ b/ml/src/dqn/reward.rs @@ -412,9 +412,9 @@ pub struct RewardFunction { impl RewardFunction { /// Create a new reward function with debug logging disabled /// - /// # Panics - /// Panics if config validation fails (e.g., use_percentage_pnl=false) - pub fn new(config: RewardConfig) -> Self { + /// # Errors + /// Returns `MLError::ConfigError` if config validation fails (e.g., use_percentage_pnl=false) + pub fn new(config: RewardConfig) -> Result { Self::new_with_debug(config, false) } @@ -424,21 +424,21 @@ impl RewardFunction { /// * `config` - Reward function configuration /// * `debug_logging` - Enable debug logging (REWARD_DEBUG, gradient norms, etc.) /// - /// # Panics - /// Panics if config validation fails (e.g., use_percentage_pnl=false) - pub fn new_with_debug(config: RewardConfig, debug_logging: bool) -> Self { + /// # Errors + /// Returns `MLError::ConfigError` if config validation fails (e.g., use_percentage_pnl=false) + pub fn new_with_debug(config: RewardConfig, debug_logging: bool) -> Result { // Fix #2: Validate config on construction to prevent gradient explosion - config.validate().expect("Invalid RewardConfig"); + config.validate()?; let normalizer = config.enable_normalization.then(|| RewardNormalizer::new()); - Self { + Ok(Self { config: config.clone(), reward_history: Vec::new(), normalizer, debug_logging, returns_buffer: std::collections::VecDeque::with_capacity(config.sharpe_window), - } + }) } /// Builder for RewardFunction @@ -1113,7 +1113,7 @@ mod tests { // Create reward function with default config let config = RewardConfig::default(); - let reward_fn = RewardFunction::new(config); + let reward_fn = RewardFunction::new(config)?; // Create states with position change: 0.0 → 1.0 (buy 1 contract) let current_state = TradingState { @@ -1198,7 +1198,7 @@ mod tests { // Simulate realistic trading scenario from audit report // Agent makes small profit ($0.10) but actual cost is $4.50 let config = RewardConfig::default(); - let mut reward_fn = RewardFunction::new(config); + let mut reward_fn = RewardFunction::new(config)?; // Initial state: $10,000 portfolio, flat position let initial_state = TradingState { @@ -1275,7 +1275,7 @@ mod tests { use crate::dqn::action_space::{ExposureLevel, OrderType, Urgency}; let config = RewardConfig::default(); - let reward_fn = RewardFunction::new(config); + let reward_fn = RewardFunction::new(config)?; // Create states with NO position change (hold scenario) let current_state = TradingState { diff --git a/ml/src/dqn/target_update.rs b/ml/src/dqn/target_update.rs index 0734e06d7..bb933763c 100644 --- a/ml/src/dqn/target_update.rs +++ b/ml/src/dqn/target_update.rs @@ -47,8 +47,14 @@ pub fn polyak_update(online_vars: &VarMap, target_vars: &VarMap, tau: f64) -> Ca tau ); - let online_data = online_vars.data().lock().unwrap(); - let mut target_data = target_vars.data().lock().unwrap(); + let online_data = online_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock online vars: {e}")))?; + let mut target_data = target_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock target vars: {e}")))?; for (name, online_tensor) in online_data.iter() { if let Some(target_tensor) = target_data.get_mut(name) { @@ -92,8 +98,14 @@ pub fn polyak_update(online_vars: &VarMap, target_vars: &VarMap, tau: f64) -> Ca /// - Potential training instability /// - Oscillating loss curves pub fn hard_update(online_vars: &VarMap, target_vars: &VarMap) -> CandleResult<()> { - let online_data = online_vars.data().lock().unwrap(); - let mut target_data = target_vars.data().lock().unwrap(); + let online_data = online_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock online vars: {e}")))?; + let mut target_data = target_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock target vars: {e}")))?; for (name, online_tensor) in online_data.iter() { target_data.insert(name.clone(), online_tensor.clone()); @@ -155,8 +167,14 @@ pub fn convergence_half_life(tau: f64) -> f64 { /// - Medium divergence (10-100): Normal during training /// - High divergence (>100): Target may be stale, consider faster τ pub fn compute_network_divergence(online_vars: &VarMap, target_vars: &VarMap) -> CandleResult { - let online_data = online_vars.data().lock().unwrap(); - let target_data = target_vars.data().lock().unwrap(); + let online_data = online_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock online vars: {e}")))?; + let target_data = target_vars + .data() + .lock() + .map_err(|e| candle_core::Error::Msg(format!("Failed to lock target vars: {e}")))?; let mut total_divergence = 0.0; let mut param_count = 0; diff --git a/ml/src/ensemble/metrics.rs b/ml/src/ensemble/metrics.rs index 9beecd867..23cd79f5f 100644 --- a/ml/src/ensemble/metrics.rs +++ b/ml/src/ensemble/metrics.rs @@ -7,6 +7,8 @@ use once_cell::sync::Lazy; use prometheus::{register_counter_vec, register_histogram_vec, CounterVec, HistogramVec}; +// Metric registration is infallible in practice; once_cell::Lazy closures cannot use `?` +#[allow(clippy::expect_used)] /// Counter for checkpoint swaps by status pub static CHECKPOINT_SWAPS_TOTAL: Lazy = Lazy::new(|| { register_counter_vec!( @@ -17,6 +19,7 @@ pub static CHECKPOINT_SWAPS_TOTAL: Lazy = Lazy::new(|| { .expect("Failed to register checkpoint_swaps_total") }); +#[allow(clippy::expect_used)] /// Histogram for checkpoint swap latency pub static CHECKPOINT_SWAP_LATENCY_MICROSECONDS: Lazy = Lazy::new(|| { register_histogram_vec!( @@ -28,6 +31,7 @@ pub static CHECKPOINT_SWAP_LATENCY_MICROSECONDS: Lazy = Lazy::new( .expect("Failed to register checkpoint_swap_latency_microseconds") }); +#[allow(clippy::expect_used)] /// Counter for checkpoint validations pub static CHECKPOINT_VALIDATION_TOTAL: Lazy = Lazy::new(|| { register_counter_vec!( @@ -38,6 +42,7 @@ pub static CHECKPOINT_VALIDATION_TOTAL: Lazy = Lazy::new(|| { .expect("Failed to register checkpoint_validation_total") }); +#[allow(clippy::expect_used)] /// Histogram for checkpoint validation latency pub static CHECKPOINT_VALIDATION_LATENCY_MILLISECONDS: Lazy = Lazy::new(|| { register_histogram_vec!( @@ -49,6 +54,7 @@ pub static CHECKPOINT_VALIDATION_LATENCY_MILLISECONDS: Lazy = Lazy .expect("Failed to register checkpoint_validation_latency_milliseconds") }); +#[allow(clippy::expect_used)] /// Histogram for validated checkpoint P99 inference latency pub static CHECKPOINT_P99_LATENCY_MICROSECONDS: Lazy = Lazy::new(|| { register_histogram_vec!( @@ -60,6 +66,7 @@ pub static CHECKPOINT_P99_LATENCY_MICROSECONDS: Lazy = Lazy::new(| .expect("Failed to register checkpoint_p99_latency_microseconds") }); +#[allow(clippy::expect_used)] /// Counter for canary monitoring results pub static CANARY_MONITORING_TOTAL: Lazy = Lazy::new(|| { register_counter_vec!( @@ -70,6 +77,7 @@ pub static CANARY_MONITORING_TOTAL: Lazy = Lazy::new(|| { .expect("Failed to register canary_monitoring_total") }); +#[allow(clippy::expect_used)] /// Histogram for canary monitoring duration pub static CANARY_MONITORING_DURATION_SECONDS: Lazy = Lazy::new(|| { register_histogram_vec!( @@ -81,6 +89,7 @@ pub static CANARY_MONITORING_DURATION_SECONDS: Lazy = Lazy::new(|| .expect("Failed to register canary_monitoring_duration_seconds") }); +#[allow(clippy::expect_used)] /// Counter for rollbacks by reason pub static CHECKPOINT_ROLLBACKS_TOTAL: Lazy = Lazy::new(|| { register_counter_vec!( diff --git a/ml/src/evaluation/report.rs b/ml/src/evaluation/report.rs index 7f15f8880..5f4da4ec9 100644 --- a/ml/src/evaluation/report.rs +++ b/ml/src/evaluation/report.rs @@ -24,9 +24,9 @@ impl BacktestReport { report.push_str("# DQN Backtest Report\n\n"); // Model info - let _ = writeln!(report, "**Model**: {}", self.model_name); + _ = writeln!(report, "**Model**: {}", self.model_name); if let Some(ref _baseline) = self.baseline_results { - let _ = writeln!(report, "**Baseline**: {}\n", self.baseline_name); + _ = writeln!(report, "**Baseline**: {}\n", self.baseline_name); } else { report.push_str("\n"); } @@ -142,7 +142,7 @@ impl BacktestReport { "--".to_owned() }; - let _ = writeln!( + _ = writeln!( report, "| {} | {} | {} | {} |", name, new_value, baseline_str, change_str diff --git a/ml/src/examples.rs b/ml/src/examples.rs index 3fd42a30e..ddd91eade 100644 --- a/ml/src/examples.rs +++ b/ml/src/examples.rs @@ -172,13 +172,13 @@ async fn run_basic_dqn_example(config: &ExampleConfig) -> Result Result b, + None => return [0.0; 5], + }; // Calculate True Range (TR) let tr = calculate_true_range(bar, prev); diff --git a/ml/src/features/alternative_bars.rs b/ml/src/features/alternative_bars.rs index 61b676085..1ebc02679 100644 --- a/ml/src/features/alternative_bars.rs +++ b/ml/src/features/alternative_bars.rs @@ -87,7 +87,6 @@ impl TickBarSampler { /// /// # Returns /// `Some(OHLCVBar)` if a bar was completed, `None` otherwise - #[allow(clippy::unwrap_in_result)] pub fn update( &mut self, price: f64, @@ -112,8 +111,8 @@ impl TickBarSampler { // Check if bar is complete (self.tick_count >= self.threshold).then(|| { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, @@ -176,7 +175,6 @@ impl VolumeBarSampler { } } - #[allow(clippy::unwrap_in_result)] pub fn update( &mut self, price: f64, @@ -197,8 +195,8 @@ impl VolumeBarSampler { (self.cumulative_volume >= self.threshold).then(|| { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, @@ -284,7 +282,6 @@ impl DollarBarSampler { self.threshold } - #[allow(clippy::unwrap_in_result)] pub fn update( &mut self, price: f64, @@ -315,8 +312,8 @@ impl DollarBarSampler { (self.cumulative_dollar >= self.threshold).then(|| { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, @@ -477,7 +474,6 @@ impl ImbalanceBarSampler { /// - Buy tick: `price > previous_price` → direction = +1 /// - Sell tick: `price < previous_price` → direction = -1 /// - Unchanged: `price == previous_price` → use last_direction (MLFinLab convention) - #[allow(clippy::unwrap_in_result)] pub fn update( &mut self, price: f64, @@ -526,8 +522,8 @@ impl ImbalanceBarSampler { // Check if bar should be emitted (absolute imbalance >= threshold) (self.cumulative_imbalance.abs() >= self.threshold).then(|| { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, @@ -656,7 +652,6 @@ impl RunBarSampler { /// - Sell tick: `price < previous_price` → direction = -1 /// - Unchanged: `price == previous_price` → no direction (run continues) /// - Direction change: Resets run_count to 1 - #[allow(clippy::unwrap_in_result)] pub fn update( &mut self, price: f64, @@ -684,8 +679,8 @@ impl RunBarSampler { // Direction changed - emit bar if threshold was met in previous run if self.run_count >= self.threshold { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, @@ -749,8 +744,8 @@ impl RunBarSampler { // Check if threshold reached AND we have a direction (self.run_count >= self.threshold && self.current_direction != 0).then(|| { let bar = OHLCVBar { - timestamp: self.first_timestamp.unwrap(), - open: self.current_open.unwrap(), + timestamp: self.first_timestamp.unwrap_or(timestamp), + open: self.current_open.unwrap_or(price), high: self.current_high, low: self.current_low, close: self.last_price, diff --git a/ml/src/features/ewma.rs b/ml/src/features/ewma.rs index f736d6150..f21db9ae3 100644 --- a/ml/src/features/ewma.rs +++ b/ml/src/features/ewma.rs @@ -115,7 +115,8 @@ impl EWMACalculator { Some(prev) => self.alpha * value + (1.0 - self.alpha) * prev, None => value, // Initialize on first value }); - self.ewma.unwrap() + // ewma is guaranteed to be Some after the match above sets it + self.ewma.unwrap_or(value) } /// Get current EWMA value diff --git a/ml/src/features/extraction.rs b/ml/src/features/extraction.rs index 0985e214c..839eab885 100644 --- a/ml/src/features/extraction.rs +++ b/ml/src/features/extraction.rs @@ -674,7 +674,10 @@ impl FeatureExtractor { fn compute_momentum(&self, period: usize) -> f64 { if self.bars.len() > period { - let curr = self.bars.back().unwrap().close; + let curr = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = self.bars[self.bars.len() - period - 1].close; if prev <= 0.0 || !prev.is_finite() { return 0.0; @@ -729,7 +732,10 @@ impl FeatureExtractor { return 0.0; } let max = self.compute_max(period); - let current = self.bars.back().unwrap().close; + let current = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; safe_clip((current - max) / current, -0.5, 0.0) } @@ -738,7 +744,10 @@ impl FeatureExtractor { return 0.0; } let min = self.compute_min(period); - let current = self.bars.back().unwrap().close; + let current = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; safe_clip((current - min) / current, 0.0, 0.5) } @@ -746,7 +755,10 @@ impl FeatureExtractor { if self.bars.len() < period { return 0.5; } - let current = self.bars.back().unwrap().close; + let current = match self.bars.back() { + Some(b) => b.close, + None => return 0.5, + }; let start = self.bars.len().saturating_sub(period); let count_below = self .bars @@ -801,7 +813,10 @@ impl FeatureExtractor { if self.bars.len() <= period { return 0.0; } - let current = self.bars.back().unwrap().close; + let current = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = self.bars[self.bars.len() - period - 1].close; if prev <= 0.0 || !prev.is_finite() { return 0.0; @@ -814,7 +829,10 @@ impl FeatureExtractor { if self.bars.len() < 3 { return 0.0; } - let curr = self.bars.back().unwrap().close; + let curr = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev1 = self.bars[self.bars.len() - 2].close; let prev2 = self.bars[self.bars.len() - 3].close; let vel1 = curr - prev1; @@ -826,34 +844,49 @@ impl FeatureExtractor { if self.bars.len() < 2 { return 0.0; } - let curr = self.bars.back().unwrap().close; + let curr = match self.bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = self.bars[self.bars.len() - 2].close; safe_clip(curr - prev, -1.0, 1.0) } fn compute_body_ratio(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let body = (bar.close - bar.open).abs(); let range = bar.high - bar.low + 1e-8; safe_clip(body / range, 0.0, 1.0) } fn compute_upper_shadow_ratio(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let upper_shadow = bar.high - bar.close.max(bar.open); let range = bar.high - bar.low + 1e-8; safe_clip(upper_shadow / range, 0.0, 1.0) } fn compute_lower_shadow_ratio(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let lower_shadow = bar.close.min(bar.open) - bar.low; let range = bar.high - bar.low + 1e-8; safe_clip(lower_shadow / range, 0.0, 1.0) } fn compute_doji_indicator(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let body = (bar.close - bar.open).abs(); let range = bar.high - bar.low + 1e-8; if body / range < 0.1 { @@ -864,7 +897,10 @@ impl FeatureExtractor { } fn compute_hammer_indicator(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let body = (bar.close - bar.open).abs(); let lower_shadow = bar.close.min(bar.open) - bar.low; let range = bar.high - bar.low + 1e-8; @@ -879,7 +915,10 @@ impl FeatureExtractor { if self.bars.len() < 2 { return 0.0; } - let curr = self.bars.back().unwrap(); + let curr = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let prev = &self.bars[self.bars.len() - 2]; let curr_body = (curr.close - curr.open).abs(); let prev_body = (prev.close - prev.open).abs(); @@ -894,7 +933,10 @@ impl FeatureExtractor { if self.bars.len() < 2 { return 0.0; } - let curr = self.bars.back().unwrap(); + let curr = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let prev = &self.bars[self.bars.len() - 2]; if prev.close <= 0.0 || !prev.close.is_finite() { return 0.0; @@ -904,7 +946,10 @@ impl FeatureExtractor { } fn compute_range_position(&self) -> f64 { - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return 0.0, + }; let range = bar.high - bar.low + 1e-8; safe_clip((bar.close - bar.low) / range, 0.0, 1.0) } @@ -913,7 +958,10 @@ impl FeatureExtractor { if self.bars.len() <= period { return 0.0; } - let curr_vol = self.bars.back().unwrap().volume; + let curr_vol = match self.bars.back() { + Some(b) => b.volume, + None => return 0.0, + }; let prev_vol = self.bars[self.bars.len() - period - 1].volume; safe_clip((curr_vol - prev_vol) / (prev_vol + 1e-8), -1.0, 1.0) } @@ -922,7 +970,10 @@ impl FeatureExtractor { if self.bars.len() < 3 { return 0.0; } - let curr = self.bars.back().unwrap().volume; + let curr = match self.bars.back() { + Some(b) => b.volume, + None => return 0.0, + }; let prev1 = self.bars[self.bars.len() - 2].volume; let prev2 = self.bars[self.bars.len() - 3].volume; let vel1 = curr - prev1; @@ -991,7 +1042,10 @@ impl FeatureExtractor { if self.bars.len() < period { return 0.5; } - let current_vol = self.bars.back().unwrap().volume; + let current_vol = match self.bars.back() { + Some(b) => b.volume, + None => return 0.5, + }; let start = self.bars.len().saturating_sub(period); let count_below = self .bars diff --git a/ml/src/features/microstructure_features.rs b/ml/src/features/microstructure_features.rs index 6dd1284f5..42188ccb4 100644 --- a/ml/src/features/microstructure_features.rs +++ b/ml/src/features/microstructure_features.rs @@ -725,9 +725,9 @@ impl PriceImpact { // Only compute impact once we have enough bars to establish direction if self.close_buffer.len() > self.delay_bars + 1 { - let old_high = self.high_buffer.pop_front().unwrap(); - let old_low = self.low_buffer.pop_front().unwrap(); - let old_close = self.close_buffer.pop_front().unwrap(); + let old_high = self.high_buffer.pop_front().unwrap_or(high); + let old_low = self.low_buffer.pop_front().unwrap_or(low); + let old_close = self.close_buffer.pop_front().unwrap_or(close); // Now close_buffer has at least delay_bars+1 elements // close_buffer[0] is the close AFTER old_close @@ -736,7 +736,7 @@ impl PriceImpact { // Alternative: Use the next close in the buffer as reference // If old_close < next_close, that's a buy (positive direction) - let next_close = self.close_buffer.front().copied().unwrap(); + let next_close = self.close_buffer.front().copied().unwrap_or(close); let direction = (next_close - old_close).signum(); let old_midpoint = (old_high + old_low) / 2.0; diff --git a/ml/src/features/pipeline.rs b/ml/src/features/pipeline.rs index 1e286a73d..7abbfe56e 100644 --- a/ml/src/features/pipeline.rs +++ b/ml/src/features/pipeline.rs @@ -125,8 +125,12 @@ impl BarsBuffer { impl std::ops::Index for BarsBuffer { type Output = OHLCVBar; + #[allow(clippy::panic)] // Index trait requires panicking on out-of-bounds fn index(&self, index: usize) -> &Self::Output { - self.get(index).expect("index out of bounds") + match self.get(index) { + Some(val) => val, + None => panic!("BarsBuffer index {index} out of bounds (len={})", self.len()), + } } } @@ -484,7 +488,6 @@ impl FeatureExtractionPipeline { } /// Compute Roll measure (bid-ask spread proxy) - #[allow(clippy::unwrap_in_result)] fn compute_roll_measure(&self) -> Result { if self.bars.len() < 2 { return Ok(0.0); @@ -515,7 +518,8 @@ impl FeatureExtractionPipeline { }; // Normalize to [0, 1] - Ok(self.safe_clip(spread / self.bars.back().unwrap().close, 0.0, 0.1)) + let last_close = self.bars.back().map_or(1.0, |b| b.close); + Ok(self.safe_clip(spread / last_close, 0.0, 0.1)) } /// Compute Amihud illiquidity ratio @@ -547,14 +551,16 @@ impl FeatureExtractionPipeline { } /// Compute Corwin-Schultz spread estimator - #[allow(clippy::unwrap_in_result)] fn compute_corwin_schultz_spread(&self) -> Result { if self.bars.len() < 2 { return Ok(0.0); } // High-low ratio method - let bar = self.bars.back().unwrap(); + let bar = match self.bars.back() { + Some(b) => b, + None => return Ok(0.0), + }; let hl_ratio = (bar.high / bar.low).ln(); // Spread estimate diff --git a/ml/src/features/price_features.rs b/ml/src/features/price_features.rs index 5fe91cd3d..8c1efb79f 100644 --- a/ml/src/features/price_features.rs +++ b/ml/src/features/price_features.rs @@ -51,7 +51,10 @@ impl PriceFeatureExtractor { } let mut features = [0.0; 15]; - let bar = bars.back().unwrap(); + let bar = match bars.back() { + Some(b) => b, + None => return features, + }; // Returns (3 features) features[0] = Self::compute_simple_return(bars); @@ -88,7 +91,10 @@ impl PriceFeatureExtractor { if bars.len() < 2 { return 0.0; } - let curr = bars.back().unwrap().close; + let curr = match bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = bars[bars.len() - 2].close; safe_clip((curr - prev) / (prev + 1e-8), -0.5, 0.5) } @@ -98,7 +104,10 @@ impl PriceFeatureExtractor { if bars.len() < 2 { return 0.0; } - let curr = bars.back().unwrap().close; + let curr = match bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = bars[bars.len() - 2].close; safe_log_return(curr, prev) } @@ -149,7 +158,10 @@ impl PriceFeatureExtractor { return 0.0; } - let bar = bars.back().unwrap(); + let bar = match bars.back() { + Some(b) => b, + None => return 0.0, + }; let prev = &bars[bars.len() - 2]; // Overnight volatility: ln(open_t / close_{t-1}) @@ -172,7 +184,10 @@ impl PriceFeatureExtractor { if bars.len() <= period { return 0.0; } - let curr = bars.back().unwrap().close; + let curr = match bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let prev = bars[bars.len() - period - 1].close; safe_clip((curr - prev) / period as f64, -10.0, 10.0) } @@ -184,7 +199,10 @@ impl PriceFeatureExtractor { } let p0 = bars[bars.len() - 3].close; let p1 = bars[bars.len() - 2].close; - let p2 = bars.back().unwrap().close; + let p2 = match bars.back() { + Some(b) => b.close, + None => return 0.0, + }; let vel1 = p2 - p1; let vel2 = p1 - p0; @@ -269,7 +287,10 @@ impl PriceFeatureExtractor { let min = prices.iter().copied().fold(f64::INFINITY, f64::min); let max = prices.iter().copied().fold(f64::NEG_INFINITY, f64::max); - let current = bars.back().unwrap().close; + let current = match bars.back() { + Some(b) => b.close, + None => return 0.5, + }; if (max - min).abs() < 1e-8 { return 0.5; diff --git a/ml/src/features/regime_adx.rs b/ml/src/features/regime_adx.rs index 03fafccb0..4bbde33e8 100644 --- a/ml/src/features/regime_adx.rs +++ b/ml/src/features/regime_adx.rs @@ -131,7 +131,10 @@ impl RegimeADXFeatures { return [0.0; 5]; } - let prev = self.prev_bar.as_ref().unwrap(); + let prev = match self.prev_bar.as_ref() { + Some(b) => b, + None => return [0.0; 5], + }; // 1. Calculate True Range (TR) let tr = self.calculate_true_range(bar, prev); diff --git a/ml/src/features/statistical_features.rs b/ml/src/features/statistical_features.rs index 69ed34a53..b75531b03 100644 --- a/ml/src/features/statistical_features.rs +++ b/ml/src/features/statistical_features.rs @@ -311,7 +311,10 @@ impl StatisticalFeatureExtractor { return 0.5; // Neutral } - let current = bars.back().unwrap().close; + let current = match bars.back() { + Some(b) => b.close, + None => return 0.5, + }; let min = Self::compute_rolling_min(bars, period); let max = Self::compute_rolling_max(bars, period); diff --git a/ml/src/features/volume_features.rs b/ml/src/features/volume_features.rs index 109e974b3..78c7fc96f 100644 --- a/ml/src/features/volume_features.rs +++ b/ml/src/features/volume_features.rs @@ -132,7 +132,10 @@ impl VolumeFeatureExtractor { return 0.0; } - let bar = self.bars().back().unwrap(); + let bar = match self.bars().back() { + Some(b) => b, + None => return 0.0, + }; let sma_50 = self.compute_volume_sma(50); let ratio = (bar.volume - sma_50) / (sma_50 + 1e-8); @@ -149,7 +152,10 @@ impl VolumeFeatureExtractor { return 0.0; } - let curr_vol = self.bars().back().unwrap().volume; + let curr_vol = match self.bars().back() { + Some(b) => b.volume, + None => return 0.0, + }; let prev_vol = self.bars()[len - period - 1].volume; let roc = (curr_vol - prev_vol) / (prev_vol + 1e-8); @@ -165,7 +171,10 @@ impl VolumeFeatureExtractor { return 0.0; } - let curr = self.bars().back().unwrap().volume; + let curr = match self.bars().back() { + Some(b) => b.volume, + None => return 0.0, + }; let prev1 = self.bars()[self.bars().len() - 2].volume; let prev2 = self.bars()[self.bars().len() - 3].volume; @@ -213,7 +222,10 @@ impl VolumeFeatureExtractor { return 0.0; } - let bar = self.bars().back().unwrap(); + let bar = match self.bars().back() { + Some(b) => b, + None => return 0.0, + }; let vwap = self.compute_vwap(20); let deviation = (bar.close - vwap) / (bar.close + 1e-8); @@ -246,7 +258,10 @@ impl VolumeFeatureExtractor { return 0.5; // Neutral } - let current_vol = self.bars().back().unwrap().volume; + let current_vol = match self.bars().back() { + Some(b) => b.volume, + None => return 0.5, + }; let start = self.bars().len() - period; let count_below = self diff --git a/ml/src/hyperopt/adapters/mamba2.rs b/ml/src/hyperopt/adapters/mamba2.rs index 50c6aab4b..dd212778e 100644 --- a/ml/src/hyperopt/adapters/mamba2.rs +++ b/ml/src/hyperopt/adapters/mamba2.rs @@ -474,20 +474,13 @@ impl Mamba2Trainer { /// /// # Returns /// - /// Price in original scale (e.g., $5000-6000 for ES futures) - /// - /// # Panics - /// - /// Panics if called before training (normalization params not set) - pub fn denormalize_prediction(&self, normalized: f64) -> f64 { - let min = self - .target_min - .expect("Normalization params not set - call train_with_params first"); - let max = self - .target_max - .expect("Normalization params not set - call train_with_params first"); + /// `Some(price)` in original scale (e.g., $5000-6000 for ES futures), + /// or `None` if normalization params have not been set (i.e., training has not run yet). + pub fn denormalize_prediction(&self, normalized: f64) -> Option { + let min = self.target_min?; + let max = self.target_max?; - normalized * (max - min) + min + Some(normalized * (max - min) + min) } /// Load and prepare training data from Parquet @@ -964,7 +957,7 @@ impl HyperparameterOptimizable for Mamba2Trainer { self.prefetch_count ); tokio::runtime::Runtime::new() - .unwrap() + .map_err(|e| MLError::ModelError(format!("Failed to create async runtime: {}", e)))? .block_on(self.train_with_async_loading( &mut model, &train_data, @@ -976,7 +969,7 @@ impl HyperparameterOptimizable for Mamba2Trainer { } else { info!("Using synchronous data loading"); tokio::runtime::Runtime::new() - .unwrap() + .map_err(|e| MLError::ModelError(format!("Failed to create async runtime: {}", e)))? .block_on(model.train( &train_data, &val_data, @@ -1237,15 +1230,14 @@ mod tests { }; // Test denormalization - assert!((trainer.denormalize_prediction(0.0) - 5000.0).abs() < 1e-6); - assert!((trainer.denormalize_prediction(1.0) - 6000.0).abs() < 1e-6); - assert!((trainer.denormalize_prediction(0.5) - 5500.0).abs() < 1e-6); - assert!((trainer.denormalize_prediction(0.25) - 5250.0).abs() < 1e-6); + assert!((trainer.denormalize_prediction(0.0).unwrap_or(0.0) - 5000.0).abs() < 1e-6); + assert!((trainer.denormalize_prediction(1.0).unwrap_or(0.0) - 6000.0).abs() < 1e-6); + assert!((trainer.denormalize_prediction(0.5).unwrap_or(0.0) - 5500.0).abs() < 1e-6); + assert!((trainer.denormalize_prediction(0.25).unwrap_or(0.0) - 5250.0).abs() < 1e-6); } #[test] - #[should_panic(expected = "Normalization params not set")] - fn test_denormalize_before_training() { + fn test_denormalize_before_training_returns_none() { // Create trainer without normalization params let trainer = Mamba2Trainer { parquet_file: PathBuf::from("dummy.parquet"), @@ -1266,8 +1258,8 @@ mod tests { trial_counter: 0, }; - // Should panic - trainer.denormalize_prediction(0.5); + // Should return None when normalization params not set + assert!(trainer.denormalize_prediction(0.5).is_none()); } #[test] diff --git a/ml/src/hyperopt/adapters/tft.rs b/ml/src/hyperopt/adapters/tft.rs index 00f045d52..b317d8a73 100644 --- a/ml/src/hyperopt/adapters/tft.rs +++ b/ml/src/hyperopt/adapters/tft.rs @@ -451,7 +451,10 @@ impl HyperparameterOptimizable for TFTTrainer { .map_err(|e| MLError::ModelError(format!("Failed to create async runtime: {}", e)))?; let training_metrics = runtime - .block_on(trainer.train_from_parquet(self.parquet_file.to_str().unwrap())) + .block_on(trainer.train_from_parquet( + self.parquet_file.to_str() + .ok_or_else(|| MLError::ModelError("Parquet file path contains invalid UTF-8".to_owned()))?, + )) .map_err(|e| MLError::TrainingError(format!("TFT training failed: {}", e)))?; // Map from crate::trainers::tft::TrainingMetrics to TFTMetrics diff --git a/ml/src/hyperopt/early_stopping.rs b/ml/src/hyperopt/early_stopping.rs index ca469662d..027fe5df3 100644 --- a/ml/src/hyperopt/early_stopping.rs +++ b/ml/src/hyperopt/early_stopping.rs @@ -1181,7 +1181,7 @@ impl TrialObserver for EarlyStoppingObserver { // Update global statistics self.trial_best_losses.push(final_loss); - if self.baseline_val_loss.is_none() || final_loss < self.baseline_val_loss.unwrap() { + if self.baseline_val_loss.is_none() || final_loss < self.baseline_val_loss.unwrap_or(f64::INFINITY) { self.baseline_val_loss = Some(final_loss); info!("New baseline loss: {:.6}", final_loss); } diff --git a/ml/src/hyperopt/optimizer.rs b/ml/src/hyperopt/optimizer.rs index 005aa32f6..dd5589bb1 100644 --- a/ml/src/hyperopt/optimizer.rs +++ b/ml/src/hyperopt/optimizer.rs @@ -311,7 +311,9 @@ impl ArgminOptimizer { }; // Find best initial point to start optimization - let trials = trial_results.lock().unwrap().clone(); + let trials = trial_results.lock().map_err(|e| { + anyhow::anyhow!("Failed to lock trial results: {}", e) + })?.clone(); let best_initial = trials .iter() .min_by(|a, b| { @@ -395,8 +397,12 @@ impl ArgminOptimizer { // Extract final results let trials = match Arc::try_unwrap(trial_results) { - Ok(mutex) => mutex.into_inner().unwrap(), - Err(arc) => arc.lock().unwrap().clone(), + Ok(mutex) => mutex.into_inner().map_err(|e| { + anyhow::anyhow!("Failed to unwrap trial results mutex: {}", e) + })?, + Err(arc) => arc.lock().map_err(|e| { + anyhow::anyhow!("Failed to lock trial results: {}", e) + })?.clone(), }; if trials.is_empty() { @@ -473,7 +479,9 @@ impl ArgminOptimizer { // Record trial result { - let mut results = trial_results.lock().unwrap(); + let mut results = trial_results.lock().map_err(|e| { + anyhow::anyhow!("Failed to lock trial results: {}", e) + })?; results.push(TrialResult { trial_num, params, @@ -579,7 +587,6 @@ where type Param = Vec; type Output = f64; - #[allow(clippy::unwrap_in_result)] fn cost(&self, param: &Self::Param) -> Result { // Increment observer trial count FIRST self.observer.increment_trial(); @@ -628,7 +635,9 @@ where info!(" Parameters (converted): {:?}", params); // Train model with parameters - let mut model = self.model.lock().unwrap(); + let mut model = self.model.lock().map_err(|e| { + argmin::core::Error::msg(format!("Failed to lock model: {}", e)) + })?; let metrics = match model.train_with_params(params.clone()) { Ok(m) => m, Err(e) => { @@ -652,7 +661,9 @@ where // Record trial result { - let mut results = self.trial_results.lock().unwrap(); + let mut results = self.trial_results.lock().map_err(|e| { + argmin::core::Error::msg(format!("Failed to lock trial results: {}", e)) + })?; results.push(TrialResult { trial_num, params, diff --git a/ml/src/hyperopt/traits.rs b/ml/src/hyperopt/traits.rs index 827a672f4..2dbf3d2ce 100644 --- a/ml/src/hyperopt/traits.rs +++ b/ml/src/hyperopt/traits.rs @@ -409,14 +409,16 @@ impl OptimizationResult

{ /// # Panics /// /// Panics if `trials` is empty (should never happen in practice) + #[allow(clippy::indexing_slicing)] // assert! guarantees non-empty; min_by always returns Some pub fn from_trials(trials: Vec>) -> Self { assert!(!trials.is_empty(), "Cannot create result from empty trials"); // Find best trial (minimum objective) + // assert! guarantees non-empty, so min_by always returns Some let best_trial = trials .iter() .min_by(|a, b| a.objective.partial_cmp(&b.objective).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap(); + .unwrap_or(&trials[0]); // Build convergence plot data let mut best_so_far = f64::INFINITY; @@ -438,27 +440,35 @@ impl OptimizationResult

{ } } - /// Get the trial with the best objective - pub fn best_trial(&self) -> &TrialResult

{ + /// Get the trial with the best objective. + /// + /// Returns `None` if `all_trials` is empty (should not happen for well-constructed results). + pub fn best_trial(&self) -> Option<&TrialResult

> { self.all_trials .iter() .min_by(|a, b| a.objective.partial_cmp(&b.objective).unwrap_or(std::cmp::Ordering::Equal)) - .unwrap() } - /// Get improvement from first to best trial + /// Get improvement from first to best trial. + /// + /// Returns `0.0` if there are no trials. pub fn total_improvement(&self) -> f64 { - let first = self.all_trials.first().unwrap().objective; - self.best_objective - first + match self.all_trials.first() { + Some(first) => self.best_objective - first.objective, + None => 0.0, + } } - /// Get improvement percentage + /// Get improvement percentage. + /// + /// Returns `0.0` if there are no trials or the first trial objective is near zero. pub fn improvement_percentage(&self) -> f64 { - let first = self.all_trials.first().unwrap().objective; - if first.abs() < 1e-10 { - return 0.0; + match self.all_trials.first() { + Some(first) if first.objective.abs() >= 1e-10 => { + ((first.objective - self.best_objective) / first.objective.abs()) * 100.0 + } + _ => 0.0, } - ((first - self.best_objective) / first.abs()) * 100.0 } } diff --git a/ml/src/inference.rs b/ml/src/inference.rs index 4ad363585..78e298ede 100644 --- a/ml/src/inference.rs +++ b/ml/src/inference.rs @@ -38,88 +38,91 @@ use prometheus::{ Histogram, HistogramOpts, IntGauge, }; -// SAFETY: Prometheus metric registration with string literal names is infallible. -// Metric constructors with static string names are infallible in practice. -// The unwrap_or_else chains handle the theoretical case where registration -// collides with an existing metric of the same name. -// The inner expect() is safe: only fails if static metric name is empty (impossible here). -lazy_static! { - static ref ML_PREDICTIONS_COUNTER: Counter = register_counter!( - "foxhunt_ml_predictions_total", - "Total ML predictions generated" - ).unwrap_or_else(|_| { - Counter::new("foxhunt_ml_predictions_total_fallback", "Fallback ML predictions counter") - .unwrap_or_else(|_| Counter::new("ml_predictions_fallback2", "Double fallback").expect("infallible: static metric name")) - }); +// Metric registration is infallible in practice with static string names. +// Wrapped in a module to allow `clippy::expect_used` — lazy_static closures cannot use `?`. +#[allow(clippy::expect_used)] +mod inference_metrics { + use super::*; - static ref ML_INFERENCE_LATENCY: Histogram = register_histogram!( - HistogramOpts::new( - "foxhunt_ml_inference_latency_microseconds", - "ML inference latency in microseconds" - ).buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) - ).unwrap_or_else(|_| { - Histogram::with_opts(HistogramOpts::new( - "foxhunt_ml_inference_latency_fallback", - "Fallback ML inference latency" - )).unwrap_or_else(|_| Histogram::with_opts(HistogramOpts::new("ml_latency_fallback2", "Double fallback")).expect("infallible: static metric name")) - }); + lazy_static! { + pub(super) static ref ML_PREDICTIONS_COUNTER: Counter = register_counter!( + "foxhunt_ml_predictions_total", + "Total ML predictions generated" + ).unwrap_or_else(|_| { + Counter::new("foxhunt_ml_predictions_total_fallback", "Fallback ML predictions counter") + .unwrap_or_else(|_| Counter::new("ml_predictions_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_MODEL_ACCURACY_GAUGE: Gauge = register_gauge!( - "foxhunt_ml_model_accuracy", - "Current ML model accuracy" - ).unwrap_or_else(|_| { - Gauge::new("foxhunt_ml_model_accuracy_fallback", "Fallback ML model accuracy") - .unwrap_or_else(|_| Gauge::new("ml_accuracy_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_INFERENCE_LATENCY: Histogram = register_histogram!( + HistogramOpts::new( + "foxhunt_ml_inference_latency_microseconds", + "ML inference latency in microseconds" + ).buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + ).unwrap_or_else(|_| { + Histogram::with_opts(HistogramOpts::new( + "foxhunt_ml_inference_latency_fallback", + "Fallback ML inference latency" + )).unwrap_or_else(|_| Histogram::with_opts(HistogramOpts::new("ml_latency_fallback2", "Double fallback")).expect("infallible: static metric name")) + }); - static ref ML_CONFIDENCE_GAUGE: Gauge = register_gauge!( - "foxhunt_ml_prediction_confidence", - "Average ML prediction confidence" - ).unwrap_or_else(|_| { - Gauge::new("foxhunt_ml_prediction_confidence_fallback", "Fallback ML confidence gauge") - .unwrap_or_else(|_| Gauge::new("ml_confidence_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_MODEL_ACCURACY_GAUGE: Gauge = register_gauge!( + "foxhunt_ml_model_accuracy", + "Current ML model accuracy" + ).unwrap_or_else(|_| { + Gauge::new("foxhunt_ml_model_accuracy_fallback", "Fallback ML model accuracy") + .unwrap_or_else(|_| Gauge::new("ml_accuracy_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_DRIFT_SCORE_GAUGE: Gauge = register_gauge!( - "foxhunt_ml_model_drift_score", - "Current ML model drift score" - ).unwrap_or_else(|_| { - Gauge::new("foxhunt_ml_model_drift_score_fallback", "Fallback ML drift score gauge") - .unwrap_or_else(|_| Gauge::new("ml_drift_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_CONFIDENCE_GAUGE: Gauge = register_gauge!( + "foxhunt_ml_prediction_confidence", + "Average ML prediction confidence" + ).unwrap_or_else(|_| { + Gauge::new("foxhunt_ml_prediction_confidence_fallback", "Fallback ML confidence gauge") + .unwrap_or_else(|_| Gauge::new("ml_confidence_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_CACHE_HITS_COUNTER: Counter = register_counter!( - "foxhunt_ml_cache_hits_total", - "Total ML prediction cache hits" - ).unwrap_or_else(|_| { - Counter::new("foxhunt_ml_cache_hits_total_fallback", "Fallback ML cache hits counter") - .unwrap_or_else(|_| Counter::new("ml_cache_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_DRIFT_SCORE_GAUGE: Gauge = register_gauge!( + "foxhunt_ml_model_drift_score", + "Current ML model drift score" + ).unwrap_or_else(|_| { + Gauge::new("foxhunt_ml_model_drift_score_fallback", "Fallback ML drift score gauge") + .unwrap_or_else(|_| Gauge::new("ml_drift_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_SAFETY_VIOLATIONS_COUNTER: Counter = register_counter!( - "foxhunt_ml_safety_violations_total", - "Total ML safety violations detected" - ).unwrap_or_else(|_| { - Counter::new("foxhunt_ml_safety_violations_total_fallback", "Fallback ML safety violations counter") - .unwrap_or_else(|_| Counter::new("ml_safety_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_CACHE_HITS_COUNTER: Counter = register_counter!( + "foxhunt_ml_cache_hits_total", + "Total ML prediction cache hits" + ).unwrap_or_else(|_| { + Counter::new("foxhunt_ml_cache_hits_total_fallback", "Fallback ML cache hits counter") + .unwrap_or_else(|_| Counter::new("ml_cache_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_MODELS_LOADED_GAUGE: IntGauge = register_int_gauge!( - "foxhunt_ml_models_loaded", - "Number of ML models currently loaded" - ).unwrap_or_else(|_| { - IntGauge::new("foxhunt_ml_models_loaded_fallback", "Fallback ML models loaded gauge") - .unwrap_or_else(|_| IntGauge::new("ml_models_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_SAFETY_VIOLATIONS_COUNTER: Counter = register_counter!( + "foxhunt_ml_safety_violations_total", + "Total ML safety violations detected" + ).unwrap_or_else(|_| { + Counter::new("foxhunt_ml_safety_violations_total_fallback", "Fallback ML safety violations counter") + .unwrap_or_else(|_| Counter::new("ml_safety_fallback2", "Double fallback").expect("infallible: static metric name")) + }); - static ref ML_MEMORY_USAGE_GAUGE: Gauge = register_gauge!( - "foxhunt_ml_memory_usage_bytes", - "ML inference memory usage in bytes" - ).unwrap_or_else(|_| { - Gauge::new("foxhunt_ml_memory_usage_bytes_fallback", "Fallback ML memory usage gauge") - .unwrap_or_else(|_| Gauge::new("ml_memory_fallback2", "Double fallback").expect("infallible: static metric name")) - }); + pub(super) static ref ML_MODELS_LOADED_GAUGE: IntGauge = register_int_gauge!( + "foxhunt_ml_models_loaded", + "Number of ML models currently loaded" + ).unwrap_or_else(|_| { + IntGauge::new("foxhunt_ml_models_loaded_fallback", "Fallback ML models loaded gauge") + .unwrap_or_else(|_| IntGauge::new("ml_models_fallback2", "Double fallback").expect("infallible: static metric name")) + }); + + pub(super) static ref ML_MEMORY_USAGE_GAUGE: Gauge = register_gauge!( + "foxhunt_ml_memory_usage_bytes", + "ML inference memory usage in bytes" + ).unwrap_or_else(|_| { + Gauge::new("foxhunt_ml_memory_usage_bytes_fallback", "Fallback ML memory usage gauge") + .unwrap_or_else(|_| Gauge::new("ml_memory_fallback2", "Double fallback").expect("infallible: static metric name")) + }); + } } +use inference_metrics::*; /// Real inference errors (no mocks allowed) #[derive(Error, Debug)] pub enum RealInferenceError { diff --git a/ml/src/memory_optimization/qat.rs b/ml/src/memory_optimization/qat.rs index e5d363a7b..2fad69ee2 100644 --- a/ml/src/memory_optimization/qat.rs +++ b/ml/src/memory_optimization/qat.rs @@ -155,8 +155,12 @@ impl QuantizationObserver { let batch_max = data.iter().cloned().fold(f32::NEG_INFINITY, f32::max); // Update running statistics with EMA - let mut min_lock = self.running_min.lock().unwrap(); - let mut max_lock = self.running_max.lock().unwrap(); + let mut min_lock = self.running_min.lock().map_err(|e| { + MLError::ModelError(format!("Failed to lock running_min: {}", e)) + })?; + let mut max_lock = self.running_max.lock().map_err(|e| { + MLError::ModelError(format!("Failed to lock running_max: {}", e)) + })?; match (*min_lock, *max_lock) { (Some(current_min), Some(current_max)) => { @@ -192,10 +196,9 @@ impl QuantizationObserver { /// # Returns /// * `Some((min, max))` - Calibrated min/max values /// * `None` - Not calibrated yet - #[allow(clippy::unwrap_in_result)] pub fn get_min_max(&self) -> Option<(f32, f32)> { - let min_lock = self.running_min.lock().unwrap(); - let max_lock = self.running_max.lock().unwrap(); + let min_lock = self.running_min.lock().ok()?; + let max_lock = self.running_max.lock().ok()?; match (*min_lock, *max_lock) { (Some(min), Some(max)) => Some((min, max)), @@ -210,8 +213,12 @@ impl QuantizationObserver { /// Reset observer statistics pub fn reset(&mut self) { - *self.running_min.lock().unwrap() = None; - *self.running_max.lock().unwrap() = None; + if let Ok(mut min_lock) = self.running_min.lock() { + *min_lock = None; + } + if let Ok(mut max_lock) = self.running_max.lock() { + *max_lock = None; + } self.num_observations.store(0, Ordering::Relaxed); self.calibrated.store(false, Ordering::Relaxed); } diff --git a/ml/src/observability/metrics.rs b/ml/src/observability/metrics.rs index a5b0df0eb..4bfd1d9c3 100644 --- a/ml/src/observability/metrics.rs +++ b/ml/src/observability/metrics.rs @@ -560,9 +560,10 @@ impl MLMetricsCollector { } } +#[allow(clippy::expect_used)] // new() registers metrics on a fresh registry, which is infallible impl Default for MLMetricsCollector { fn default() -> Self { - Self::new().expect("Failed to create metrics collector") + Self::new().expect("MLMetricsCollector::new() failed on a fresh prometheus Registry") } } diff --git a/ml/src/random_model.rs b/ml/src/random_model.rs index 1805e2398..1ab0607e2 100644 --- a/ml/src/random_model.rs +++ b/ml/src/random_model.rs @@ -162,7 +162,10 @@ impl GaussianRandomModel { pub fn predict(&self, _features: &FeatureMatrix) -> f32 { use rand_distr::{Distribution, Normal}; - let normal = Normal::new(self.mean, self.std_dev).unwrap(); + let normal = match Normal::new(self.mean, self.std_dev) { + Ok(n) => n, + Err(_) => return 0.0, + }; let value = if let Some(seed) = self.seed { let mut rng = rand::rngs::StdRng::seed_from_u64(seed); @@ -180,7 +183,10 @@ impl GaussianRandomModel { pub fn predict_batch(&self, count: usize) -> Vec { use rand_distr::{Distribution, Normal}; - let normal = Normal::new(self.mean, self.std_dev).unwrap(); + let normal = match Normal::new(self.mean, self.std_dev) { + Ok(n) => n, + Err(_) => return vec![0.0; count], + }; if let Some(seed) = self.seed { let mut rng = rand::rngs::StdRng::seed_from_u64(seed); diff --git a/ml/src/regime/orchestrator.rs b/ml/src/regime/orchestrator.rs index 1e3653663..076c228bd 100644 --- a/ml/src/regime/orchestrator.rs +++ b/ml/src/regime/orchestrator.rs @@ -340,7 +340,10 @@ impl RegimeOrchestrator { let confidence = (adx / 100.0).clamp(0.0, 1.0); // ADX is 0-100, normalize to 0-1 // Step 5: Persist to database - let timestamp = bars.last().unwrap().timestamp; + let timestamp = bars.last().ok_or(OrchestratorError::InsufficientData { + required: 1, + actual: 0, + })?.timestamp; sqlx::query!( r#" diff --git a/ml/src/regime/volatile.rs b/ml/src/regime/volatile.rs index 4b90c8132..7b165b019 100644 --- a/ml/src/regime/volatile.rs +++ b/ml/src/regime/volatile.rs @@ -234,7 +234,10 @@ impl VolatileClassifier { return bar.high - bar.low; } - let prev = self.bars.back().unwrap(); + let prev = match self.bars.back() { + Some(bar) => bar, + None => return bar.high - bar.low, + }; let high_low = bar.high - bar.low; let high_close = (bar.high - prev.close).abs(); let low_close = (bar.low - prev.close).abs(); diff --git a/ml/src/registry/mod.rs b/ml/src/registry/mod.rs index 3911870fa..917389122 100644 --- a/ml/src/registry/mod.rs +++ b/ml/src/registry/mod.rs @@ -231,8 +231,11 @@ impl ModelRegistryTrait for InMemoryModelRegistry { .map(|(id, _)| id.clone()) .collect(); - // Must drop the version borrow before modifying others - let version = versions.get_mut(version_id).expect("just checked"); + // Must drop the version borrow before modifying others. + // version_id was confirmed to exist via the get_mut() + ok_or_else() above. + let version = versions.get_mut(version_id).ok_or_else(|| { + RegistryError::VersionNotFound(version_id.to_string()) + })?; version.stage = to_stage; version.promoted_by = Some(promoted_by.to_string()); diff --git a/ml/src/risk/kelly_position_sizing_service.rs b/ml/src/risk/kelly_position_sizing_service.rs index 0637d6a53..b8aec18a0 100644 --- a/ml/src/risk/kelly_position_sizing_service.rs +++ b/ml/src/risk/kelly_position_sizing_service.rs @@ -685,7 +685,7 @@ impl KellyPositionSizingService { ); if risk_fraction < 1.0 { - let _ = write!( + _ = write!( rationale, "Applied {:.0}% risk tolerance adjustment. ", risk_fraction * 100.0 diff --git a/ml/src/stress_testing/market_simulator.rs b/ml/src/stress_testing/market_simulator.rs index f21122b16..d6bf34320 100644 --- a/ml/src/stress_testing/market_simulator.rs +++ b/ml/src/stress_testing/market_simulator.rs @@ -144,13 +144,14 @@ impl MarketDataSimulator { } } - #[allow(clippy::unwrap_in_result)] fn generate_market_update( &mut self, symbol: &str, rng: &mut StdRng, ) -> Result { - let state = self.symbol_states.get_mut(symbol).unwrap(); + let state = self.symbol_states.get_mut(symbol).ok_or_else(|| { + anyhow::anyhow!("Unknown symbol in market simulator: {}", symbol) + })?; // Get symbol-specific configuration for realistic behavior let default_sim_config = SimulationConfig::default(); @@ -172,7 +173,7 @@ impl MarketDataSimulator { let current_f64 = state.current_price.to_f64(); let price_change = current_f64 * (drift + diffusion); let new_price = (current_f64 + price_change).max(0.01); - state.current_price = Price::from_f64(new_price).unwrap(); + state.current_price = Price::from_f64(new_price).unwrap_or_default(); // Update bid/ask with realistic spread based on symbol configuration let spread_bps = rng.gen_range(symbol_config.min_spread_bps..=symbol_config.max_spread_bps); diff --git a/ml/src/stress_testing/mod.rs b/ml/src/stress_testing/mod.rs index 3766d9b0b..dc0174a57 100644 --- a/ml/src/stress_testing/mod.rs +++ b/ml/src/stress_testing/mod.rs @@ -476,11 +476,11 @@ pub struct MarketDataUpdate { impl MarketDataUpdate { pub fn spread(&self) -> Price { - Price::from_f64(self.ask.to_f64() - self.bid.to_f64()).unwrap() + 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() + Price::from_f64((self.bid.to_f64() + self.ask.to_f64()) / 2.0).unwrap_or_default() } } diff --git a/ml/src/tft/quantized_tft.rs b/ml/src/tft/quantized_tft.rs index 0facd97e9..2daa21f86 100644 --- a/ml/src/tft/quantized_tft.rs +++ b/ml/src/tft/quantized_tft.rs @@ -512,7 +512,9 @@ impl QuantizedTemporalFusionTransformer { )); } - let attention_weights = self.attention_weights.as_ref().unwrap(); + let attention_weights = self.attention_weights.as_ref().ok_or_else(|| { + MLError::ModelError("Attention weights unexpectedly None after validation".to_owned()) + })?; debug!("🔄 Building attention weight cache..."); @@ -587,7 +589,9 @@ impl QuantizedTemporalFusionTransformer { )); } - let attention_weights = self.attention_weights.as_ref().unwrap(); + let attention_weights = self.attention_weights.as_ref().ok_or_else(|| { + MLError::ModelError("Attention weights unexpectedly None after validation".to_owned()) + })?; let q_weight = self .quantizer .dequantize_tensor(&attention_weights.q_weight)?; diff --git a/ml/src/tft/quantized_vsn.rs b/ml/src/tft/quantized_vsn.rs index f05747b40..06de078cd 100644 --- a/ml/src/tft/quantized_vsn.rs +++ b/ml/src/tft/quantized_vsn.rs @@ -69,7 +69,9 @@ impl QuantizedVariableSelectionNetwork { let mut quantized_weights = HashMap::new(); // Get all variables from varmap - let var_data = varmap.data().lock().unwrap(); + let var_data = varmap.data().lock().map_err(|e| { + MLError::ModelError(format!("Failed to lock varmap data: {}", e)) + })?; let vars = var_data.clone(); drop(var_data); diff --git a/ml/src/trainers/dqn/config.rs b/ml/src/trainers/dqn/config.rs index 69b3d12e0..e085e1960 100644 --- a/ml/src/trainers/dqn/config.rs +++ b/ml/src/trainers/dqn/config.rs @@ -219,7 +219,10 @@ impl DQNAgentType { Self::Standard(agent) => agent.get_q_network_vars().clone(), Self::RegimeConditional(agent) => { // For regime-conditional, return trending head vars as representative - agent.get_trending_head().unwrap().get_q_network_vars().clone() + match agent.get_trending_head() { + Some(head) => head.get_q_network_vars().clone(), + None => VarMap::new(), + } } } } diff --git a/ml/src/trainers/dqn/monitoring.rs b/ml/src/trainers/dqn/monitoring.rs index 9f7f7684d..faccb6747 100644 --- a/ml/src/trainers/dqn/monitoring.rs +++ b/ml/src/trainers/dqn/monitoring.rs @@ -273,8 +273,8 @@ impl TrainingMonitor { let total = self.episode_lengths.len(); let mean = self.episode_lengths.iter().sum::() as f64 / total as f64; - let min = *self.episode_lengths.iter().min().unwrap(); - let max = *self.episode_lengths.iter().max().unwrap(); + let min = self.episode_lengths.iter().min().copied().unwrap_or(0); + let max = self.episode_lengths.iter().max().copied().unwrap_or(0); // Calculate std dev let variance = self.episode_lengths.iter() diff --git a/ml/src/trainers/dqn/trainer.rs b/ml/src/trainers/dqn/trainer.rs index 2c4104d78..1819031eb 100644 --- a/ml/src/trainers/dqn/trainer.rs +++ b/ml/src/trainers/dqn/trainer.rs @@ -406,7 +406,7 @@ impl DQNTrainer { sharpe_weight: Decimal::ZERO, // WAVE 26 P1.3: Disabled by default sharpe_window: 20, // WAVE 26 P1.3: Standard 20-period window }; - let reward_fn = RewardFunction::new_with_debug(reward_config, debug_logging); + let reward_fn = RewardFunction::new_with_debug(reward_config, debug_logging)?; // WAVE 1.1: Initialize triple barrier engine (max 1000 active trackers) let triple_barrier = Arc::new(RwLock::new(TripleBarrierEngine::new(1000))); diff --git a/ml/src/trainers/tft/trainer.rs b/ml/src/trainers/tft/trainer.rs index 900487c4e..617410862 100644 --- a/ml/src/trainers/tft/trainer.rs +++ b/ml/src/trainers/tft/trainer.rs @@ -905,16 +905,15 @@ impl TFTTrainer { // Export comprehensive QAT metrics for Prometheus if let Some(qat_metrics) = self.export_qat_metrics() { - final_metrics.qat_metrics = Some(qat_metrics.clone()); - self.state.qat_metrics = Some(qat_metrics); - info!( "QAT Metrics Exported: {} observers, {:.1}% calibration, {:.4} avg scale, {:.4} quant error", - self.state.qat_metrics.as_ref().unwrap().observer_count, + qat_metrics.observer_count, self.state.qat_calibration_progress, - self.state.qat_metrics.as_ref().unwrap().scale_statistics.mean, + qat_metrics.scale_statistics.mean, self.state.qat_fake_quant_error ); + final_metrics.qat_metrics = Some(qat_metrics.clone()); + self.state.qat_metrics = Some(qat_metrics); } // Estimate INT8 accuracy: assume 1% accuracy loss per 0.1 quantization error @@ -1695,7 +1694,10 @@ impl TFTTrainer { // Save quantized weights to SafeTensors format info!("💾 Saving INT8 checkpoint: {}.safetensors", checkpoint_name); - save_quantized_weights(&quantized_weights, checkpoint_path.to_str().unwrap())?; + let checkpoint_str = checkpoint_path.to_str().ok_or_else(|| { + MLError::CheckpointError("Checkpoint path contains invalid UTF-8".to_owned()) + })?; + save_quantized_weights(&quantized_weights, checkpoint_str)?; // Save metadata JSON sidecar let metadata = CheckpointMetadata { @@ -1901,7 +1903,10 @@ impl TFTTrainer { "💾 Saving QAT-INT8 checkpoint: {}.safetensors", checkpoint_name ); - save_quantized_weights(&quantized_weights, checkpoint_path.to_str().unwrap())?; + let checkpoint_str = checkpoint_path.to_str().ok_or_else(|| { + MLError::CheckpointError("Checkpoint path contains invalid UTF-8".to_owned()) + })?; + save_quantized_weights(&quantized_weights, checkpoint_str)?; // Save metadata JSON sidecar let metadata = CheckpointMetadata { diff --git a/ml/src/trainers/tft_parquet.rs b/ml/src/trainers/tft_parquet.rs index 0d9b51dbe..b0251776f 100644 --- a/ml/src/trainers/tft_parquet.rs +++ b/ml/src/trainers/tft_parquet.rs @@ -45,8 +45,8 @@ impl TFTTrainer { // by load_training_data_from_parquet() for later denormalization info!( "Target normalization applied: mean={:.2}, std={:.2}", - self.target_mean.unwrap(), - self.target_std.unwrap() + self.target_mean.unwrap_or(0.0), + self.target_std.unwrap_or(1.0) ); info!( diff --git a/ml/src/trainers/tlob.rs b/ml/src/trainers/tlob.rs index ebc688c7f..68be0154f 100644 --- a/ml/src/trainers/tlob.rs +++ b/ml/src/trainers/tlob.rs @@ -308,7 +308,10 @@ impl TLOBTrainer { let (val_loss, mae) = self.validate_epoch(&val_sequences).await?; // Calculate metrics - let elapsed = self.start_time.unwrap().elapsed().as_secs_f64(); + let elapsed = self.start_time + .unwrap_or_else(|| Instant::now()) + .elapsed() + .as_secs_f64(); let grad_norm = self.calculate_gradient_norm()?; let metrics = TLOBTrainingMetrics { diff --git a/ml/src/training/orchestrator.rs b/ml/src/training/orchestrator.rs index 7805fa59d..2c978f0e2 100644 --- a/ml/src/training/orchestrator.rs +++ b/ml/src/training/orchestrator.rs @@ -190,7 +190,10 @@ impl UnifiedTrainingOrchestrator { .config .checkpoint_dir .join(format!("{}_best", model.model_type())); - model.save_checkpoint(checkpoint_path.to_str().unwrap())?; + let path_str = checkpoint_path.to_str().ok_or_else(|| { + MLError::ModelError("Invalid UTF-8 in checkpoint path".to_owned()) + })?; + model.save_checkpoint(path_str)?; info!("New best validation loss: {:.6}, checkpoint saved", vl); } else { self.epochs_without_improvement += 1; @@ -217,7 +220,10 @@ impl UnifiedTrainingOrchestrator { epoch, self.current_step, )); - model.save_checkpoint(checkpoint_path.to_str().unwrap())?; + let path_str = checkpoint_path.to_str().ok_or_else(|| { + MLError::ModelError("Invalid UTF-8 in checkpoint path".to_owned()) + })?; + model.save_checkpoint(path_str)?; debug!("Checkpoint saved at epoch {}", epoch + 1); } } diff --git a/ml/tests/mamba2_hyperopt_edge_cases.rs b/ml/tests/mamba2_hyperopt_edge_cases.rs index 38b1c4ec6..0c8509010 100644 --- a/ml/tests/mamba2_hyperopt_edge_cases.rs +++ b/ml/tests/mamba2_hyperopt_edge_cases.rs @@ -178,8 +178,10 @@ fn test_full_training_pipeline() { // Test denormalization let pred = trainer.denormalize_prediction(0.5); + assert!(pred.is_some(), "Denormalized prediction should be Some after training"); + let pred_val = pred.unwrap_or(0.0); assert!( - pred.is_finite() && pred > 0.0, + pred_val.is_finite() && pred_val > 0.0, "Denormalized prediction should be valid" ); } diff --git a/ml/tests/portfolio_integration_tests.rs b/ml/tests/portfolio_integration_tests.rs index 2c821df32..c35c78658 100644 --- a/ml/tests/portfolio_integration_tests.rs +++ b/ml/tests/portfolio_integration_tests.rs @@ -168,7 +168,7 @@ fn test_portfolio_features_dimension() -> anyhow::Result<()> { fn test_pnl_reward_nonzero() -> anyhow::Result<()> { // Setup: Create reward function with default config let config = RewardConfig::default(); - let mut reward_fn = RewardFunction::new(config); + let mut reward_fn = RewardFunction::new(config)?; // Create current state (no position) let current_state = TradingState::from_normalized( @@ -246,7 +246,7 @@ fn test_pnl_reward_nonzero() -> anyhow::Result<()> { #[test] fn test_pnl_calculation_accuracy() -> anyhow::Result<()> { let config = RewardConfig::default(); - let mut reward_fn = RewardFunction::new(config); + let mut reward_fn = RewardFunction::new(config)?; // Test case 1: Small profit (1%) let current_state = TradingState::from_normalized( @@ -487,7 +487,7 @@ fn test_edge_case_large_positions() -> anyhow::Result<()> { #[test] fn test_reward_function_receives_portfolio() -> anyhow::Result<()> { let config = RewardConfig::default(); - let mut reward_fn = RewardFunction::new(config); + let mut reward_fn = RewardFunction::new(config)?; // Create states with different portfolio values let state_low = TradingState::from_normalized( @@ -600,7 +600,7 @@ fn test_integration_batch_rewards() -> anyhow::Result<()> { use ml::dqn::reward::calculate_batch_rewards; let config = RewardConfig::default(); - let mut reward_fn = RewardFunction::new(config); + let mut reward_fn = RewardFunction::new(config)?; // Create batch of state transitions let actions = vec![buy_action(), hold_action(), sell_action()]; @@ -786,8 +786,8 @@ fn test_reward_calculation_consistency() -> anyhow::Result<()> { let config = RewardConfig::default(); // Calculate same reward twice - let mut reward_fn1 = RewardFunction::new(config.clone()); - let mut reward_fn2 = RewardFunction::new(config); + let mut reward_fn1 = RewardFunction::new(config.clone())?; + let mut reward_fn2 = RewardFunction::new(config)?; let current_state = TradingState::from_normalized( vec![0.0; 16],