diff --git a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs index 1ba19692f..d6c7a5e39 100644 --- a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs +++ b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs @@ -176,7 +176,7 @@ impl Default for ExperienceCollectorConfig { fill_spread_cost_frac: 0.50, fill_spread_capture_frac: 0.50, fill_simulation_enabled: false, - use_dsr: false, + use_dsr: true, dsr_eta: 0.01, n_steps: 1, } diff --git a/crates/ml/src/ensemble/adapters/dqn.rs b/crates/ml/src/ensemble/adapters/dqn.rs index f8b65e433..b7089830a 100644 --- a/crates/ml/src/ensemble/adapters/dqn.rs +++ b/crates/ml/src/ensemble/adapters/dqn.rs @@ -249,6 +249,9 @@ mod tests { state_dim: 56, num_actions: 5, hidden_dims: vec![64, 64], + // Disable branching for inference adapter tests — the adapter uses + // the plain dueling forward path, not the branching heads. + use_branching: false, ..Default::default() } } diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 572ad9ec5..331eb1db6 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -3180,6 +3180,11 @@ impl HyperparameterOptimizable for DQNTrainer { let raw_state_dim: usize = if self.mbp10_data_dir.is_some() { 53 } else { 45 }; let state_dim = crate::dqn::mixed_precision::align_dim_for_tensor_cores(raw_state_dim, &device); + // OFI offset: validation bars start after training bars in the global OFI array. + // Without this, walk-forward evaluation gets zero OFI features (train/eval mismatch). + let ofi_offset = self.preloaded_training_data.as_ref().map_or(0, |d| d.len()); + let ofi_enabled = self.mbp10_data_dir.is_some(); + let total_bars = val_close_prices.len(); let window_size = total_bars / 3; // Same per-window size as before #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] @@ -3221,8 +3226,9 @@ impl HyperparameterOptimizable for DQNTrainer { m } Err(e) => { - tracing::warn!("GPU backtest failed, falling back to CPU: {e}"); - None + return Err(MLError::ModelError(format!( + "GPU backtest FAILED (no CPU fallback): {e}" + ))); } } } else { @@ -3295,7 +3301,16 @@ impl HyperparameterOptimizable for DQNTrainer { for (i, (feature_vec, _target)) in chunk.iter().enumerate() { let close_price = val_close_prices[abs_start + i]; - match internal_trainer.convert_to_state_vec(feature_vec, close_price) { + // OFI fix: inject real OFI features during eval to match training. + // Without this, eval gets zero OFI → train/eval feature mismatch. + let convert_result = if ofi_enabled { + internal_trainer.convert_to_state_vec_with_ofi( + feature_vec, close_price, ofi_offset + abs_start + i, + ) + } else { + internal_trainer.convert_to_state_vec(feature_vec, close_price) + }; + match convert_result { Ok(sv) => { flat_states.extend_from_slice(&sv); // Zero-pad raw state (53/45) to aligned dim (56/48) diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index f47844043..f2f377335 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -1244,8 +1244,8 @@ impl DQNHyperparameters { // BUG #7: Minimum profit factor for trade execution minimum_profit_factor: 1.5, // Default: 50% margin above breakeven - // DSR: disabled by default (opt-in via hyperopt) - use_dsr: false, + // DSR: enabled by default — directly optimizes Sharpe ratio (Moody & Saffell 2001) + use_dsr: true, dsr_eta: 0.01, // ~100-step effective EMA window // OFI: disabled by default (requires MBP-10 data) diff --git a/crates/ml/src/trainers/dqn/trainer.rs b/crates/ml/src/trainers/dqn/trainer.rs index 5bc63d673..7a37f43cb 100644 --- a/crates/ml/src/trainers/dqn/trainer.rs +++ b/crates/ml/src/trainers/dqn/trainer.rs @@ -257,10 +257,13 @@ pub struct DQNTrainer { /// Multi-GPU configuration for data-parallel training (None = single GPU) multi_gpu: Option, - /// Pre-computed OFI features per bar (indexed by training data position) + /// Pre-computed OFI features per bar (indexed by global bar position). /// Populated during data loading when MBP-10 order book data is available. /// Passed as `regime_features` in `TradingState::from_normalized()`. pub(crate) ofi_features: Option>, + /// Number of training bars (OFI offset for validation data). + /// val_data[i] corresponds to ofi_features[ofi_val_offset + i]. + pub(crate) ofi_val_offset: usize, // Phase C: Fill simulation and smart order routing /// Fill simulator for order type-dependent execution modeling @@ -976,6 +979,7 @@ impl DQNTrainer { // OFI features: populated during data loading when MBP-10 data is available ofi_features: None, + ofi_val_offset: 0, // Phase C: Fill simulation and smart order routing fill_simulator: FillSimulator::default(), @@ -1071,6 +1075,7 @@ impl DQNTrainer { ); // Store validation data for loss computation + self.ofi_val_offset = training_data.len(); self.val_data = val_data; // Use the common training loop (Wave 12 Group 3 refactor) @@ -1114,6 +1119,7 @@ impl DQNTrainer { ); // Store validation data for loss computation + self.ofi_val_offset = training_data.len(); self.val_data = val_data; // Use the common training loop (Wave 12 Group 3 refactor) @@ -1140,6 +1146,7 @@ impl DQNTrainer { val_data.len() ); + self.ofi_val_offset = training_data.len(); self.val_data = val_data; self.train_with_data_full_loop(training_data, checkpoint_callback) .await @@ -1330,11 +1337,13 @@ impl DQNTrainer { }; let close_price = rust_decimal::Decimal::try_from(current_close) .unwrap_or(rust_decimal::Decimal::ZERO); - let state = self.feature_vector_to_state(feature_vec, Some(close_price))?; + // OFI fix: pass global OFI index so validation gets real OFI features + let ofi_idx = self.ofi_val_offset + i; + let state = self.feature_vector_to_state_with_ofi(feature_vec, Some(close_price), Some(ofi_idx))?; let next_close_price = rust_decimal::Decimal::try_from(next_close).unwrap_or(rust_decimal::Decimal::ZERO); - let next_state = self.feature_vector_to_state(feature_vec, Some(next_close_price))?; + let next_state = self.feature_vector_to_state_with_ofi(feature_vec, Some(next_close_price), Some(ofi_idx))?; // P4: Write state vector directly into the flat buffer at the correct row offset. // Raw dims (e.g. 45 or 53) are shorter than aligned (48 or 56); trailing @@ -2337,8 +2346,9 @@ impl DQNTrainer { true } Err(e) => { - warn!("GPU zero-roundtrip collection failed, falling back to CPU path: {e}"); - false + return Err(MLError::ModelError(format!( + "GPU zero-roundtrip collection FAILED (no CPU fallback): {e}" + ))); } } } else { @@ -5940,6 +5950,22 @@ impl DQNTrainer { Ok(trading_state.to_vector()) } + /// Convert feature vector to flat state Vec with OFI features at the given index. + /// + /// Same as `convert_to_state_vec` but injects OFI features from the preloaded + /// array at `ofi_index`, preventing train/eval feature mismatch. + pub fn convert_to_state_vec_with_ofi( + &self, + feature_vec: &FeatureVector, + close_price: f64, + ofi_index: usize, + ) -> Result> { + let close = rust_decimal::Decimal::try_from(close_price) + .map_err(|e| CommonError::validation(&format!("Invalid close price: {}", e)))?; + let trading_state = self.feature_vector_to_state_with_ofi(feature_vec, Some(close), Some(ofi_index))?; + Ok(trading_state.to_vector()) + } + /// Update portfolio tracker to reflect current position from backtest engine. /// /// Called between chunks so the next chunk's portfolio features