fix: backtracking uses val_Sharpe (not training Sharpe) for plateau detection
ROOT CAUSE: run_backtracking_epoch_end received epoch_sharpe (training Sharpe from experience collection, ~0.65-0.79 oscillating) instead of val_Sharpe (deterministic backtest, 33.49 frozen for 56 epochs). Training Sharpe oscillates even when the model is frozen → sharpe_frozen was always false → AND condition never met → backtracking never triggered despite 56 consecutive frozen epochs in train-2tgs7. Fixes: 1. Pass val_sharpe (-val_loss) to run_backtracking_epoch_end 2. prev_val_sharpe field on BacktrackingState (not sharpe_history) 3. improvement_rate uses val_Sharpe delta (not training Sharpe) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user