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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-12 10:06:17 +01:00
parent 368975c1cd
commit 23157e9faa

View File

@@ -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<f64> = 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<f64> = 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()
);
}