fix(dqn): fix OFI train/eval mismatch, enable DSR by default
- Walk-forward CPU backtest was calling feature_vector_to_state() without OFI index, producing zero OFI features during evaluation while training had real OFI — creating a silent train/eval feature mismatch - Add convert_to_state_vec_with_ofi() public method on DQNTrainer - Add ofi_val_offset field to track training data length for OFI indexing - compute_validation_loss() now passes OFI index to validation states - Hyperopt CPU eval path now uses convert_to_state_vec_with_ofi() - Enable DSR (Differential Sharpe Ratio) by default in both config and GPU experience collector — aligns with hyperopt which always uses DSR - Fix ensemble adapter test to explicitly disable branching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -257,10 +257,13 @@ pub struct DQNTrainer {
|
||||
/// Multi-GPU configuration for data-parallel training (None = single GPU)
|
||||
multi_gpu: Option<crate::cuda_pipeline::multi_gpu::MultiGpuConfig>,
|
||||
|
||||
/// 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<Vec<[f64; 8]>>,
|
||||
/// 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<f32> 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<Vec<f32>> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user