fix(ml): fix position mask panic + stale 45-action comments after DQN 5-action refactor

apply_position_mask() in factored_q_network.rs looped 0..45 against a
5-wide Q-values tensor — would panic at runtime. Changed to 0..5 using
ExposureLevel::from_index() directly. Updated stale "45 actions" comments
in 5 files (dqn.rs, reward.rs, hyperopt/adapters/dqn.rs, curriculum.rs).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-06 20:52:55 +01:00
parent e0bee7640b
commit faeae5ef01
5 changed files with 18 additions and 17 deletions

View File

@@ -235,8 +235,8 @@ impl Default for DQNConfig {
dueling_hidden_dim: 128,
use_distributional: true, // BUG #36 FIXED: C51 re-enabled
num_atoms: 51,
v_min: -10.0, // Widened: [-2,2] compressed Q-values, [-10,10] gives C51 room to separate 45 actions
v_max: 10.0, // Widened: [-2,2] compressed Q-values, [-10,10] gives C51 room to separate 45 actions
v_min: -10.0, // Widened: [-2,2] compressed Q-values, [-10,10] gives C51 room to separate actions
v_max: 10.0, // Widened: [-2,2] compressed Q-values, [-10,10] gives C51 room to separate actions
use_noisy_nets: false,
noisy_sigma_init: 0.5,
enable_q_value_clipping: true,

View File

@@ -97,9 +97,9 @@ impl FactoredQNetwork {
})
}
/// Forward pass: compute Q-values for all 45 actions directly
/// Forward pass: compute Q-values for all exposure actions
///
/// Returns q_values [batch, 45]
/// Returns q_values [batch, num_actions]
pub fn forward(&self, state: &Tensor) -> Result<Tensor, MLError> {
// DEBUG: Log input shape
tracing::debug!("FactoredQNetwork input shape: {:?}", state.dims());
@@ -118,7 +118,7 @@ impl FactoredQNetwork {
// DEBUG: Log hidden representation shape
tracing::debug!("Hidden representation shape: {:?}", hidden.dims());
// Joint head: hidden → 45 Q-values
// Joint head: hidden → Q-values (one per exposure level)
let q_values = self
.joint_head
.forward(&hidden)
@@ -137,14 +137,14 @@ impl FactoredQNetwork {
}
/// Select greedy action (argmax on 45 Q-values)
/// Select greedy action (argmax on Q-values across exposure levels)
pub fn select_greedy_action(&self, state: &Tensor) -> Result<FactoredAction, MLError> {
let q_values = self.forward(state)?;
// DEBUG: Log Q-value shape before argmax
tracing::debug!("Pre-argmax Q-value shape: {:?}", q_values.dims());
// Argmax across all 45 actions
// Argmax across exposure actions
let action_idx = q_values
.argmax(1)
.map_err(|e| MLError::ModelError(format!("Argmax failed: {}", e)))?
@@ -207,11 +207,12 @@ impl FactoredQNetwork {
.to_vec2::<f32>()
.map_err(|e| MLError::ModelError(format!("Failed to convert q_values to vec: {}", e)))?;
// Mask invalid actions (iterate all 45 action indices)
// Mask invalid exposure actions (0..5: Short100, Short50, Flat, Long50, Long100)
for batch_idx in 0..batch_size {
for action_idx in 0..45 {
let action = FactoredAction::from_index(action_idx)?;
let target_position = action.exposure.target_exposure();
for action_idx in 0..5 {
let exposure = crate::dqn::action_space::ExposureLevel::from_index(action_idx)
.map_err(|e| MLError::ModelError(format!("Invalid exposure index {}: {}", action_idx, e)))?;
let target_position = exposure.target_exposure();
// Check if this would exceed ±100% limit
if (current_position + target_position).abs() > 1.0 {

View File

@@ -522,7 +522,7 @@ impl RewardFunction {
};
// Calculate diversity bonus (in-training regularization)
// Entropy is normalized [0,1] over 45 actions. Threshold 0.3 ≈ ~4 distinct actions.
// Entropy is normalized [0,1] over 5 exposure actions. Threshold 0.3 ≈ ~2 distinct actions.
let entropy = calculate_entropy(recent_actions);
let entropy_threshold = Decimal::try_from(0.3).unwrap_or(Decimal::ZERO);
let diversity_bonus = if entropy < entropy_threshold {

View File

@@ -424,8 +424,8 @@ impl Default for DQNParams {
// =============================================================================
use_distributional: true, // ENABLED: BUG #36 fixed, full Rainbow DQN C51
num_atoms: 51, // Wave 2.3: Rainbow DQN standard
v_min: -10.0, // Widened for C51 action separation (45 actions need wider support)
v_max: 10.0, // Widened for C51 action separation (45 actions need wider support)
v_min: -10.0, // Widened for C51 action separation
v_max: 10.0, // Widened for C51 action separation
use_noisy_nets: true, // Wave 8: Default ENABLED for full Rainbow DQN
noisy_sigma_init: 0.5, // Wave 2.4: Rainbow DQN standard
minimum_profit_factor: 1.5, // BUG #7: Default 50% margin above breakeven

View File

@@ -4,9 +4,9 @@
//!
//! | Phase | Exposure Actions | Count | Regimes | Gate |
//! |-------|-----------------|-------|---------|------|
//! | 1: Basic | Short50, Flat, Long50 | 27 | Ranging only | Sharpe > 0.5 for 3 folds |
//! | 2: FullPosition | All 5 exposures | 45 | Ranging + Trending | Sharpe > 0.3 for 2 folds |
//! | 3: AllRegimes | All 45 | 45 | All regimes | Terminal |
//! | 1: Basic | Short50, Flat, Long50 | 3 | Ranging only | Sharpe > 0.5 for 3 folds |
//! | 2: FullPosition | All 5 exposures | 5 | Ranging + Trending | Sharpe > 0.3 for 2 folds |
//! | 3: AllRegimes | All 5 | 5 | All regimes | Terminal |
//!
//! The scheduler tracks consecutive qualifying folds and advances the phase
//! when the gate condition is met. Action masks combine curriculum restrictions