From 23157e9faa00e1a997a85d0e2ea3acb7a917caae Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 12 Mar 2026 10:06:17 +0100 Subject: [PATCH] fix(dqn): normalize VaR/CVaR to percentage returns using initial capital pnl_history stores raw dollar rewards but VaR calculation treated them as percentage returns, producing nonsensical -500% VaR values. Now divides by initial_capital before calculating percentiles. Co-Authored-By: Claude Opus 4.6 --- crates/ml/src/trainers/dqn/trainer.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/ml/src/trainers/dqn/trainer.rs b/crates/ml/src/trainers/dqn/trainer.rs index fe85ee2c3..a2dd6d51b 100644 --- a/crates/ml/src/trainers/dqn/trainer.rs +++ b/crates/ml/src/trainers/dqn/trainer.rs @@ -3931,8 +3931,14 @@ impl DQNTrainer { // WAVE 3.11: Calculate and log VaR/CVaR from PnL history if self.pnl_history.len() > 20 { - let returns: Vec = self.pnl_history.iter().copied().collect(); - + // Normalize raw dollar PnL to percentage returns relative to initial capital + let capital = self.hyperparams.initial_capital as f64; + let returns: Vec = if capital > 0.0 { + self.pnl_history.iter().map(|&pnl| pnl / capital).collect() + } else { + self.pnl_history.iter().copied().collect() + }; + // Calculate VaR/CVaR at 95% and 99% confidence levels // confidence_level=0.05 means we're looking at the worst 5% of returns (95% VaR) let (var_95, cvar_95) = calculate_var_cvar(&returns, 0.05); @@ -3942,10 +3948,10 @@ impl DQNTrainer { "Epoch {}/{}: Risk Metrics - VaR(95%)={:.4}%, CVaR(95%)={:.4}%, VaR(99%)={:.4}%, CVaR(99%)={:.4}% (from {} PnL samples)", epoch + 1, self.hyperparams.epochs, - var_95 * 100.0, // Convert to percentage - cvar_95 * 100.0, // Convert to percentage - var_99 * 100.0, // Convert to percentage - cvar_99 * 100.0, // Convert to percentage + var_95 * 100.0, + cvar_95 * 100.0, + var_99 * 100.0, + cvar_99 * 100.0, returns.len() ); }