feat: reward config pipeline — DQNHyperparameters + TOML profiles

Add 7 composite reward fields to DQNHyperparameters: w_dsr, w_pnl,
w_dd, w_idle, dd_threshold, loss_aversion, time_decay_rate.

Add RewardSection to training_profile.rs with Option<f64> fields and
apply_to() mapping. Add [reward] section to all 3 DQN TOML profiles
(production, smoketest, hyperopt) with identical defaults.

Remove hold_reward from ExperienceSection (replaced by w_idle).
Add 7 reward search bounds to SearchSpaceSection and bound() match.
Add 7 reward phase_fast defaults to PhaseFastSection.

hold_penalty kept as deprecated field for hyperopt adapter compat
(Task 4 will clean it up).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-22 20:50:23 +01:00
parent aa272666a1
commit af940671bc
5 changed files with 136 additions and 8 deletions

View File

@@ -61,6 +61,24 @@ gradient_accumulation_steps = [1, 1] # fixed at 1 for now
# IQN dual-head
iqn_lambda = [0.0, 2.0]
# Composite reward weights
w_dsr = [0.1, 2.0]
w_pnl = [0.0, 1.0]
w_dd = [0.0, 5.0]
w_idle = [0.0, 0.1]
dd_threshold = [0.01, 0.10]
loss_aversion = [1.0, 3.0]
time_decay_rate = [0.0001, 0.005]
[reward]
w_dsr = 1.0
w_pnl = 0.3
w_dd = 1.0
w_idle = 0.01
dd_threshold = 0.02
loss_aversion = 1.5
time_decay_rate = 0.0005
[fixed]
use_branching = true # GPU pipeline requires branching
use_double_dqn = true
@@ -84,3 +102,10 @@ num_atoms = 11
branch_hidden_dim = 64
dueling_hidden_dim = 128
v_range = 20.0
w_dsr = 1.0
w_pnl = 0.3
w_dd = 1.0
w_idle = 0.01
dd_threshold = 0.02
loss_aversion = 1.5
time_decay_rate = 0.0005

View File

@@ -63,4 +63,12 @@ min_loss_improvement_pct = 0.1
[experience]
initial_capital = 100000.0
tx_cost_multiplier = 0.0001
hold_reward = 0.001
[reward]
w_dsr = 1.0
w_pnl = 0.3
w_dd = 1.0
w_idle = 0.01
dd_threshold = 0.02
loss_aversion = 1.5
time_decay_rate = 0.0005

View File

@@ -27,3 +27,12 @@ min_epochs_before_stopping = 5
[experience]
gpu_n_episodes = 16
gpu_timesteps_per_episode = 50
[reward]
w_dsr = 1.0
w_pnl = 0.3
w_dd = 1.0
w_idle = 0.01
dd_threshold = 0.02
loss_aversion = 1.5
time_decay_rate = 0.0005

View File

@@ -813,8 +813,24 @@ pub struct DQNHyperparameters {
pub plateau_window: usize,
/// Minimum epochs before early stopping can trigger (default: 50)
pub min_epochs_before_stopping: usize,
/// Small negative penalty encourages action diversity (Bug #3 fix)
/// Deprecated: replaced by `w_idle` in composite reward. Kept for hyperopt adapter compat.
pub hold_penalty: f64,
// GPU composite reward weights (8-component composite in CUDA kernel)
/// DSR (Sharpe) weight for composite reward
pub w_dsr: f64,
/// Normalized PnL weight for composite reward
pub w_pnl: f64,
/// Drawdown penalty weight for composite reward
pub w_dd: f64,
/// Idle penalty weight for composite reward
pub w_idle: f64,
/// Drawdown tolerance before penalty kicks in (fraction, e.g. 0.02 = 2%)
pub dd_threshold: f64,
/// Asymmetric loss scaling factor (prospect theory, default 1.5)
pub loss_aversion: f64,
/// Position staleness rent per step held
pub time_decay_rate: f64,
/// Use Huber loss instead of MSE (more robust to outliers)
pub use_huber_loss: bool,
/// Huber loss delta threshold (default: 1.0)
@@ -1205,7 +1221,14 @@ impl DQNHyperparameters {
min_loss_improvement_pct: 2.0,
plateau_window: 30,
min_epochs_before_stopping: 50,
hold_penalty: -0.001,
hold_penalty: -0.001, // deprecated: kept for hyperopt adapter compat
w_dsr: 1.0,
w_pnl: 0.3,
w_dd: 1.0,
w_idle: 0.01,
dd_threshold: 0.02,
loss_aversion: 1.5,
time_decay_rate: 0.0005,
use_huber_loss: true, // Default: Huber loss enabled (more robust)
huber_delta: 100.0, // BUG #12 FIX: Scale delta 100x for gradient explosion fix (was 1.0)
use_double_dqn: true, // Default: Double DQN enabled (prevents overestimation bias)

View File

@@ -138,6 +138,25 @@ pub struct EarlyStoppingSection {
pub min_loss_improvement_pct: Option<f64>,
}
/// GPU composite reward weights.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RewardSection {
/// DSR (Sharpe) weight
pub w_dsr: Option<f64>,
/// Normalized PnL weight
pub w_pnl: Option<f64>,
/// Drawdown penalty weight
pub w_dd: Option<f64>,
/// Idle penalty weight
pub w_idle: Option<f64>,
/// Drawdown tolerance before penalty (fraction)
pub dd_threshold: Option<f64>,
/// Asymmetric loss scaling factor (prospect theory)
pub loss_aversion: Option<f64>,
/// Position staleness rent per step
pub time_decay_rate: Option<f64>,
}
/// GPU experience collection parameters.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ExperienceSection {
@@ -146,8 +165,6 @@ pub struct ExperienceSection {
pub initial_capital: Option<f64>,
/// Maps to `DQNHyperparameters::transaction_cost_multiplier`.
pub tx_cost_multiplier: Option<f64>,
/// Maps to `DQNHyperparameters::hold_penalty`.
pub hold_reward: Option<f64>,
}
/// Walk-forward cross-validation window configuration.
@@ -178,6 +195,7 @@ pub struct DqnTrainingProfile {
pub risk: Option<RiskSection>,
pub early_stopping: Option<EarlyStoppingSection>,
pub experience: Option<ExperienceSection>,
pub reward: Option<RewardSection>,
pub walk_forward: Option<WalkForwardSection>,
}
@@ -219,6 +237,14 @@ pub struct SearchSpaceSection {
pub branch_hidden_dim: Option<[f64; 2]>,
pub gradient_accumulation_steps: Option<[f64; 2]>,
pub iqn_lambda: Option<[f64; 2]>,
// Composite reward weights
pub w_dsr: Option<[f64; 2]>,
pub w_pnl: Option<[f64; 2]>,
pub w_dd: Option<[f64; 2]>,
pub w_idle: Option<[f64; 2]>,
pub dd_threshold: Option<[f64; 2]>,
pub loss_aversion: Option<[f64; 2]>,
pub time_decay_rate: Option<[f64; 2]>,
}
/// PSO optimizer configuration.
@@ -241,6 +267,14 @@ pub struct PhaseFastSection {
pub branch_hidden_dim: Option<f64>,
pub dueling_hidden_dim: Option<f64>,
pub v_range: Option<f64>,
// Composite reward weights (fixed in phase fast)
pub w_dsr: Option<f64>,
pub w_pnl: Option<f64>,
pub w_dd: Option<f64>,
pub w_idle: Option<f64>,
pub dd_threshold: Option<f64>,
pub loss_aversion: Option<f64>,
pub time_decay_rate: Option<f64>,
}
/// Two-phase hyperopt configuration.
@@ -353,6 +387,13 @@ impl HyperoptProfile {
"branch_hidden_dim" => ss.branch_hidden_dim,
"gradient_accumulation_steps" => ss.gradient_accumulation_steps,
"iqn_lambda" => ss.iqn_lambda,
"w_dsr" => ss.w_dsr,
"w_pnl" => ss.w_pnl,
"w_dd" => ss.w_dd,
"w_idle" => ss.w_idle,
"dd_threshold" => ss.dd_threshold,
"loss_aversion" => ss.loss_aversion,
"time_decay_rate" => ss.time_decay_rate,
_ => None,
};
match val {
@@ -648,7 +689,6 @@ impl DqnTrainingProfile {
// [experience]
// TOML: tx_cost_multiplier → hp: transaction_cost_multiplier
// TOML: hold_reward → hp: hold_penalty
// TOML: initial_capital → hp: initial_capital (f32 in hp, f64 in TOML)
if let Some(ref ex) = self.experience {
if let Some(v) = ex.gpu_n_episodes {
@@ -663,8 +703,31 @@ impl DqnTrainingProfile {
if let Some(v) = ex.tx_cost_multiplier {
hp.transaction_cost_multiplier = v;
}
if let Some(v) = ex.hold_reward {
hp.hold_penalty = v;
}
// [reward]
// GPU composite reward weights — 7 fields map directly to DQNHyperparameters
if let Some(ref rw) = self.reward {
if let Some(v) = rw.w_dsr {
hp.w_dsr = v;
}
if let Some(v) = rw.w_pnl {
hp.w_pnl = v;
}
if let Some(v) = rw.w_dd {
hp.w_dd = v;
}
if let Some(v) = rw.w_idle {
hp.w_idle = v;
}
if let Some(v) = rw.dd_threshold {
hp.dd_threshold = v;
}
if let Some(v) = rw.loss_aversion {
hp.loss_aversion = v;
}
if let Some(v) = rw.time_decay_rate {
hp.time_decay_rate = v;
}
}