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:
jgrusewski
2026-04-15 17:51:50 +02:00
parent 255cb9d26a
commit e1d548bdf3
2 changed files with 13 additions and 16 deletions

View File

@@ -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,

View File

@@ -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 {