diff --git a/crates/ml/src/trainers/dqn/trainer/mod.rs b/crates/ml/src/trainers/dqn/trainer/mod.rs index 8376d39f0..5663e8fd7 100644 --- a/crates/ml/src/trainers/dqn/trainer/mod.rs +++ b/crates/ml/src/trainers/dqn/trainer/mod.rs @@ -110,6 +110,8 @@ pub(crate) struct BacktrackingState { pub lr_boost_remaining: usize, /// Original LR (saved before LR boost). pub original_lr: f64, + /// Previous epoch's val_Sharpe for freeze detection (NOT training Sharpe). + pub prev_val_sharpe: f32, } impl BacktrackingState { @@ -132,6 +134,7 @@ impl BacktrackingState { min_improvement_rate: 0.1, // min 0.1 val_Sharpe/epoch improvement best_sharpe: f32::NEG_INFINITY, best_epoch: 0, + prev_val_sharpe: 0.0, temp_boost_remaining: 0, lr_boost_remaining: 0, original_lr: 0.0, diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 7fd9b92ad..3d745b8e6 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -575,7 +575,10 @@ impl DQNTrainer { ).await?; // Task 10: Trajectory backtracking -- detect plateau, rewind, perturb - if self.run_backtracking_epoch_end(epoch, log_output.epoch_sharpe)? { + // Use val_Sharpe (deterministic backtest), NOT epoch_sharpe (training Sharpe). + // Training Sharpe oscillates even when the model is frozen, preventing detection. + let val_sharpe_for_backtrack = -log_output.val_loss; // val_loss = -sharpe + if self.run_backtracking_epoch_end(epoch, val_sharpe_for_backtrack)? { // PLATEAU_EXHAUSTED -- save best model and exit info!( "PLATEAU_EXHAUSTED: restoring best model from epoch {} before exit", @@ -2453,27 +2456,18 @@ impl DQNTrainer { } // 2. Save checkpoint when improving - // Use second-to-last sharpe (last is current epoch, already pushed before this runs) - let prev_sharpe = if self.sharpe_history.len() >= 2 { - self.sharpe_history[self.sharpe_history.len() - 2] as f32 - } else { - 0.0 - }; - let improvement_rate = val_sharpe_f32 - prev_sharpe; // per epoch + // Use prev_val_sharpe from backtracking state (tracks val_Sharpe, not training Sharpe) + let improvement_rate = val_sharpe_f32 - self.backtracking.prev_val_sharpe; if improvement_rate > 0.01 && !self.backtracking.route_active { self.save_backtracking_checkpoint(epoch, improvement_rate, val_sharpe_f32)?; } // 3. Detect plateau: BOTH Q-gap frozen AND val_Sharpe stagnating. - // OR alone is too aggressive — stable Q-gap + rising val_Sharpe is healthy convergence, - // not a plateau. Only intervene when NOTHING improves. + // Uses val_Sharpe (deterministic backtest), NOT training Sharpe. + // Training Sharpe oscillates even when the model is truly frozen. let q_gap_frozen = self.is_q_gap_frozen(); - let sharpe_frozen = if self.sharpe_history.len() >= 2 { - let prev = self.sharpe_history[self.sharpe_history.len() - 2] as f32; - (val_sharpe_f32 - prev).abs() < 0.01 - } else { - false - }; + let sharpe_frozen = (val_sharpe_f32 - self.backtracking.prev_val_sharpe).abs() < 0.01; + self.backtracking.prev_val_sharpe = val_sharpe_f32; // update for next epoch if q_gap_frozen && sharpe_frozen { self.backtracking.plateau_epochs += 1; if self.backtracking.plateau_epochs > 0 {