From 3696cb475bb96cd0acbd1c1aebdb1cd7b237763e Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 11 Apr 2026 12:12:18 +0200 Subject: [PATCH] fix: update action reporting from 9-level to 7-level ExposureLevel ShortSmall=0, ShortHalf=1, ShortFull=2, Flat=3, LongSmall=4, LongHalf=5, LongFull=6. Updates all array sizes, bounds checks, names, and distribution logic across gpu_monitoring, monitoring, training_loop, metrics, and financials. Co-Authored-By: Claude Sonnet 4.6 --- crates/ml/src/cuda_pipeline/gpu_monitoring.rs | 20 +++---- crates/ml/src/trainers/dqn/financials.rs | 47 ++++++++-------- crates/ml/src/trainers/dqn/monitoring.rs | 54 +++++++++---------- crates/ml/src/trainers/dqn/trainer/metrics.rs | 36 +++++++------ .../src/trainers/dqn/trainer/training_loop.rs | 22 ++++---- 5 files changed, 91 insertions(+), 88 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/gpu_monitoring.rs b/crates/ml/src/cuda_pipeline/gpu_monitoring.rs index aaeddf69b..5d93c29a9 100644 --- a/crates/ml/src/cuda_pipeline/gpu_monitoring.rs +++ b/crates/ml/src/cuda_pipeline/gpu_monitoring.rs @@ -26,8 +26,8 @@ pub struct MonitoringSummary { pub max_reward: f32, /// Sharpe estimate: mean_reward / reward_std (per-trade). pub sharpe_estimate: f32, - /// Per direction*magnitude combo counts (9 bins: 3 dir * 3 mag). - pub action_counts: [usize; 9], + /// Per exposure level counts (7 bins: ShortSmall=0, ShortHalf=1, ShortFull=2, Flat=3, LongSmall=4, LongHalf=5, LongFull=6). + pub action_counts: [usize; 7], /// Per-order-type counts (3: Market, LimitMaker, IoC). pub order_counts: [usize; 3], /// Per-urgency counts (3: Patient, Normal, Aggressive). @@ -53,7 +53,7 @@ impl GpuMonitoringReducer { .map_err(|e| MLError::ModelError(format!("monitoring module load: {e}")))?; let kernel_func = module.load_function("monitoring_reduce") .map_err(|e| MLError::ModelError(format!("monitoring_reduce load: {e}")))?; - let summary_buf = stream.alloc_zeros::(24) // 5 stats + 9 exp + 3 ord + 3 urg + 1 total + 3 pad + let summary_buf = stream.alloc_zeros::(24) // 5 stats + 7 exp + 3 ord + 3 urg + 1 total + 5 pad .map_err(|e| MLError::ModelError(format!("monitoring summary alloc: {e}")))?; Ok(Self { stream: Arc::clone(stream), kernel_func, summary_buf }) @@ -81,7 +81,7 @@ impl GpuMonitoringReducer { }; // Safety: rewards and actions are valid GPU slices with at least n elements. // summary_buf is pre-allocated 12-float device buffer on the same stream. - let num_actions_i32 = 9_i32; // dir*mag combos (3*3) + let num_actions_i32 = 7_i32; // exposure levels (7: ShortSmall..LongFull) let order_actions_i32 = 3_i32; // DQN_ORDER_ACTIONS let urgency_actions_i32 = 3_i32; // DQN_URGENCY_ACTIONS unsafe { @@ -113,12 +113,12 @@ impl GpuMonitoringReducer { action_counts: [ raw[5] as usize, raw[6] as usize, raw[7] as usize, raw[8] as usize, raw[9] as usize, raw[10] as usize, - raw[11] as usize, raw[12] as usize, raw[13] as usize, + raw[11] as usize, ], - order_counts: [raw[14] as usize, raw[15] as usize, raw[16] as usize], - urgency_counts: [raw[17] as usize, raw[18] as usize, raw[19] as usize], - total_experiences: raw[20] as usize, - total_trades: raw[21] as usize, + order_counts: [raw[12] as usize, raw[13] as usize, raw[14] as usize], + urgency_counts: [raw[15] as usize, raw[16] as usize, raw[17] as usize], + total_experiences: raw[18] as usize, + total_trades: raw[19] as usize, }) } } @@ -131,7 +131,7 @@ mod tests { fn test_monitoring_summary_default() { let s = MonitoringSummary::default(); assert_eq!(s.total_experiences, 0); - assert_eq!(s.action_counts, [0; 9]); + assert_eq!(s.action_counts, [0; 7]); } #[test] diff --git a/crates/ml/src/trainers/dqn/financials.rs b/crates/ml/src/trainers/dqn/financials.rs index 355e1b29b..89ef153a1 100644 --- a/crates/ml/src/trainers/dqn/financials.rs +++ b/crates/ml/src/trainers/dqn/financials.rs @@ -22,13 +22,14 @@ pub(crate) struct EpochFinancials { /// Compute financial metrics from GPU-sourced trade statistics and action counts. /// /// - `trade_stats`: per-epoch trade statistics from `collector.collect_trade_stats()` -/// - `action_counts`: 9-element array (exposure levels), grouped into BUY/SELL/HOLD +/// - `action_counts`: 7-element array (exposure levels), grouped into BUY/SELL/HOLD +/// Indices: 0=ShortSmall, 1=ShortHalf, 2=ShortFull, 3=Flat, 4=LongSmall, 5=LongHalf, 6=LongFull /// - `initial_capital`: starting equity for return calculation (default 100_000) /// - `bars_per_day`: number of bars in one trading day (390 for 1-min, 78 for 5-min, etc.) /// Used for Sharpe/Sortino annualization and return scaling. Pass from data pipeline's BarSize. pub(crate) fn compute_epoch_financials( trade_stats: &TradeStats, - action_counts: &[usize; 9], + action_counts: &[usize; 7], initial_capital: f64, bars_per_day: f64, ) -> EpochFinancials { @@ -164,15 +165,15 @@ pub(crate) fn compute_epoch_financials( // Cap at 100% — drawdown cannot exceed total liquidation let max_dd = max_dd.min(1.0); - // Action distribution: 9 exposure actions -> BUY/SELL/HOLD - // [S100, S75, S50, S25, Flat, L25, L50, L75, L100] - // 0 1 2 3 4 5 6 7 8 - // Sell = indices 0-3, Hold = index 4, Buy = indices 5-8 + // Action distribution: 7 exposure actions -> BUY/SELL/HOLD + // [ShortSmall, ShortHalf, ShortFull, Flat, LongSmall, LongHalf, LongFull] + // 0 1 2 3 4 5 6 + // Sell = indices 0-2, Hold = index 3, Buy = indices 4-6 let total_actions: usize = action_counts.iter().sum(); let (buy_pct, sell_pct, hold_pct) = if total_actions > 0 { - let sell: usize = action_counts.iter().take(4).sum(); // S100+S75+S50+S25 - let hold = *action_counts.get(4).unwrap_or(&0); // Flat - let buy: usize = action_counts.iter().skip(5).sum(); // L25+L50+L75+L100 + let sell: usize = action_counts.iter().take(3).sum(); // ShortSmall+ShortHalf+ShortFull + let hold = *action_counts.get(3).unwrap_or(&0); // Flat + let buy: usize = action_counts.iter().skip(4).sum(); // LongSmall+LongHalf+LongFull let t = total_actions as f64; (buy as f64 / t, sell as f64 / t, hold as f64 / t) } else { @@ -202,7 +203,7 @@ mod tests { #[test] fn test_empty_trade_stats() { let ts = TradeStats::default(); - let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); assert_eq!(f.total_trades, 0); assert_eq!(f.sharpe, 0.0); } @@ -220,7 +221,7 @@ mod tests { step_returns: vec![0.01, 0.02, 0.03, 0.015, 0.025], ..Default::default() }; - let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); assert_eq!(f.win_rate, 1.0); assert_eq!(f.total_trades, 5); assert!(f.sharpe > 0.0, "sharpe={}", f.sharpe); @@ -241,7 +242,7 @@ mod tests { step_returns: vec![0.01, -0.005, 0.008, -0.003, 0.005], ..Default::default() }; - let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); assert_eq!(f.total_trades, 5); assert!((f.win_rate - 0.6).abs() < 1e-10, "win_rate={}", f.win_rate); assert!(f.total_return > 0.0, "total_return={}", f.total_return); @@ -251,13 +252,13 @@ mod tests { } #[test] - fn test_action_distribution_9_actions() { - let mut actions = [0usize; 9]; - actions[0] = 10; // S100 -> SELL - actions[1] = 10; // S75 -> SELL - actions[4] = 20; // Flat -> HOLD - actions[7] = 30; // L75 -> BUY - actions[8] = 30; // L100 -> BUY + fn test_action_distribution_7_actions() { + let mut actions = [0usize; 7]; + actions[0] = 10; // ShortSmall -> SELL + actions[1] = 10; // ShortHalf -> SELL + actions[3] = 20; // Flat -> HOLD + actions[5] = 30; // LongHalf -> BUY + actions[6] = 30; // LongFull -> BUY let ts = TradeStats { total_trades: 1, winning_trades: 1, @@ -278,8 +279,8 @@ mod tests { #[test] fn test_action_distribution_all_indices_counted() { - let mut actions = [0usize; 9]; - for i in 0..9 { actions[i] = 10; } + let mut actions = [0usize; 7]; + for i in 0..7 { actions[i] = 10; } let ts = TradeStats { total_trades: 1, winning_trades: 1, @@ -323,7 +324,7 @@ mod tests { step_returns, done_flags, }; - let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); // Episode 1 drawdown: (1-0.8)*(1-0.15)*(1-0.10)*(1-0.05) ≈ 0.4131 → ~42% DD // Episode 2 starts fresh, drawdown is smaller. @@ -355,7 +356,7 @@ mod tests { step_returns: vec![0.01, -0.005, 0.008, -0.003], done_flags: vec![], // empty — no episode info }; - let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); // Should not panic and should produce a valid number assert!(f.max_drawdown >= 0.0, "max_dd={}", f.max_drawdown); assert!(f.max_drawdown <= 1.0, "max_dd={}", f.max_drawdown); diff --git a/crates/ml/src/trainers/dqn/monitoring.rs b/crates/ml/src/trainers/dqn/monitoring.rs index 84cd4c7e2..8b0069e9e 100644 --- a/crates/ml/src/trainers/dqn/monitoring.rs +++ b/crates/ml/src/trainers/dqn/monitoring.rs @@ -12,14 +12,14 @@ use crate::dqn::action_space::FactoredAction; pub(crate) struct TrainingMonitor { pub(crate) epoch: usize, pub(crate) reward_history: Vec, - pub(crate) action_counts: [usize; 9], // 9 direction*magnitude bins (3 dir * 3 mag) - pub(crate) q_value_sums: [f64; 9], // Sum of Q-values per dir*mag combo - pub(crate) q_value_counts: [usize; 9], // Count of Q-values per dir*mag combo + pub(crate) action_counts: [usize; 7], // 7 exposure levels (ShortSmall..LongFull) + pub(crate) q_value_sums: [f64; 7], // Sum of Q-values per exposure level + pub(crate) q_value_counts: [usize; 7], // Count of Q-values per exposure level pub(crate) order_type_counts: [usize; 3], // Market, LimitMaker, IoC pub(crate) urgency_counts: [usize; 3], // Patient, Normal, Aggressive - /// Factored action counts: 3 dir * 3 mag * 3 order * 3 urgency = 81 actions. - /// Index = dir * 27 + mag * 9 + order * 3 + urgency (0-80). - pub(crate) factored_action_counts: [usize; 81], + /// Factored action counts: 7 exposure * 3 order * 3 urgency = 63 actions. + /// Index = exp_idx * 9 + order * 3 + urgency (0-62). + pub(crate) factored_action_counts: [usize; 63], pub(crate) consecutive_constant_epochs: usize, // Q-value range tracking (WAVE 9-11 production monitoring) pub(crate) q_value_min: f64, @@ -37,12 +37,12 @@ impl TrainingMonitor { Self { epoch, reward_history: Vec::new(), - action_counts: [0; 9], - q_value_sums: [0.0; 9], - q_value_counts: [0; 9], + action_counts: [0; 7], + q_value_sums: [0.0; 7], + q_value_counts: [0; 7], order_type_counts: [0; 3], urgency_counts: [0; 3], - factored_action_counts: [0; 81], + factored_action_counts: [0; 63], consecutive_constant_epochs: 0, q_value_min: f64::INFINITY, q_value_max: f64::NEG_INFINITY, @@ -66,9 +66,9 @@ impl TrainingMonitor { } } - /// Add action to tracking by direction*magnitude index (0-8) + /// Add action to tracking by exposure index (0-6) pub(crate) fn track_action_by_exposure(&mut self, exposure_idx: usize) { - if exposure_idx < 9 { + if exposure_idx < 7 { self.action_counts[exposure_idx] += 1; } } @@ -81,9 +81,9 @@ impl TrainingMonitor { self.order_type_counts[order_idx] += 1; let urgency_idx = action.urgency as usize; // 0-2 self.urgency_counts[urgency_idx] += 1; - // Track composite factored action index (0-44) + // Track composite factored action index (0-62): exp(0-6) * 9 + order(0-2) * 3 + urgency(0-2) let factored_idx = exp_idx * 9 + order_idx * 3 + urgency_idx; - if factored_idx < 81 { + if factored_idx < 63 { self.factored_action_counts[factored_idx] += 1; } } @@ -102,9 +102,9 @@ impl TrainingMonitor { } } - /// Add Q-value to tracking by direction*magnitude index (0-8) + /// Add Q-value to tracking by exposure index (0-6) pub(crate) fn track_q_value_by_exposure(&mut self, exposure_idx: usize, q_value: f64) { - if exposure_idx < 9 { + if exposure_idx < 7 { self.q_value_sums[exposure_idx] += q_value; self.q_value_counts[exposure_idx] += 1; } @@ -190,7 +190,7 @@ impl TrainingMonitor { return Ok(()); } - let exposure_names = ["S_25", "S_50", "S_100", "F_25", "F_50", "F_100", "L_25", "L_50", "L_100"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; for (i, &count) in self.action_counts.iter().enumerate() { let pct = (count as f64 / total_exposure as f64) * 100.0; if pct < 5.0 { @@ -220,9 +220,9 @@ impl TrainingMonitor { /// Validate Q-value balance across exposure actions pub(crate) fn validate_q_value_balance(&self) -> Result<()> { - // Calculate average Q-value per direction*magnitude combo - let mut avg_q_values = [0.0_f64; 9]; - for i in 0..9 { + // Calculate average Q-value per exposure level + let mut avg_q_values = [0.0_f64; 7]; + for i in 0..7 { if self.q_value_counts[i] > 0 { avg_q_values[i] = self.q_value_sums[i] / self.q_value_counts[i] as f64; } @@ -233,7 +233,7 @@ impl TrainingMonitor { let min_q = avg_q_values.iter().cloned().fold(f64::INFINITY, f64::min); if (max_q - min_q).abs() > 1000.0 { - let exposure_names = ["S_25", "S_50", "S_100", "F_25", "F_50", "F_100", "L_25", "L_50", "L_100"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; warn!( "Q-VALUE DIVERGENCE at epoch {}: {}", self.epoch, @@ -252,11 +252,11 @@ impl TrainingMonitor { if self.epoch % 10 == 0 { let total_actions: usize = self.action_counts.iter().sum(); if total_actions > 0 { - let exposure_names = ["S_25", "S_50", "S_100", "F_25", "F_50", "F_100", "L_25", "L_50", "L_100"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; - // Log all 9 dir*mag actions + // Log all 7 exposure level actions debug!( - "Action Distribution [Epoch {}] - 9 Dir*Mag Actions:", + "Action Distribution [Epoch {}] - 7 Exposure Level Actions:", self.epoch ); for (idx, &count) in self.action_counts.iter().enumerate() { @@ -297,13 +297,13 @@ impl TrainingMonitor { let unique_ord = self.order_type_counts.iter().filter(|&&c| c > 0).count(); let unique_urg = self.urgency_counts.iter().filter(|&&c| c > 0).count(); debug!( - "Branch Diversity [Epoch {}]: exp={}/9 order={}/3 urgency={}/3", + "Branch Diversity [Epoch {}]: exp={}/7 order={}/3 urgency={}/3", self.epoch, unique_exp, unique_ord, unique_urg ); - // Log average Q-values per dir*mag combo + // Log average Q-values per exposure level debug!("Average Q-values [Epoch {}]:", self.epoch); - for idx in 0..9 { + for idx in 0..7 { if self.q_value_counts[idx] > 0 { let avg_q = self.q_value_sums[idx] / self.q_value_counts[idx] as f64; debug!(" [{}] {}: Q={:.4}", idx, exposure_names[idx], avg_q); diff --git a/crates/ml/src/trainers/dqn/trainer/metrics.rs b/crates/ml/src/trainers/dqn/trainer/metrics.rs index 508863fbe..93b547543 100644 --- a/crates/ml/src/trainers/dqn/trainer/metrics.rs +++ b/crates/ml/src/trainers/dqn/trainer/metrics.rs @@ -42,8 +42,8 @@ impl DQNTrainer { num_epochs: usize, training_duration: std::time::Duration, early_stopped: bool, - total_action_counts: [usize; 9], // 5 exposure levels - total_factored_action_counts: [usize; 81], // 81 factored actions + total_action_counts: [usize; 7], // 7 exposure levels + total_factored_action_counts: [usize; 63], // 63 factored actions (7 exp * 3 ord * 3 urg) ) -> Result { let final_loss = total_loss / num_epochs as f64; let avg_q_value_final = total_q_value / num_epochs as f64; @@ -82,26 +82,26 @@ impl DQNTrainer { metrics.add_metric("first_sharpe", first_sharpe); metrics.add_metric("final_val_loss", final_val_loss); - // Action metrics: use factored 81-action space if branching, else 5 exposure levels + // Action metrics: use factored 63-action space if branching, else 7 exposure levels let total_factored: usize = total_factored_action_counts.iter().sum(); let total_exposure: usize = total_action_counts.iter().sum(); let total_actions = total_factored.max(total_exposure); if total_actions > 0 { - // Factored 81-action diversity (primary metric when branching) + // Factored 63-action diversity (primary metric when branching) if total_factored > 0 { let unique_factored = total_factored_action_counts.iter() .filter(|&&count| count > 0).count(); - let factored_diversity = (unique_factored as f64 / 81.0) * 100.0; + let factored_diversity = (unique_factored as f64 / 63.0) * 100.0; metrics.add_metric("action_diversity", factored_diversity); metrics.add_metric("factored_unique_actions", unique_factored as f64); - metrics.add_metric("action_space_size", 81.0); + metrics.add_metric("action_space_size", 63.0); // Active factored actions (used >0.5% of the time) let active_threshold = (total_factored as f64 * 0.005).max(1.0); let active_count = total_factored_action_counts.iter() .filter(|&&count| count as f64 >= active_threshold) .count(); - let active_diversity_pct = (active_count as f64 / 81.0) * 100.0; + let active_diversity_pct = (active_count as f64 / 63.0) * 100.0; metrics.add_metric("active_actions_count", active_count as f64); metrics.add_metric("active_diversity_pct", active_diversity_pct); @@ -123,18 +123,18 @@ impl DQNTrainer { let top5_coverage_pct = (top5_count as f64 / total_factored as f64) * 100.0; metrics.add_metric("top5_coverage_pct", top5_coverage_pct); } else { - // Fallback: exposure-only (9 levels) + // Fallback: exposure-only (7 levels) let unique_actions = total_action_counts.iter() .filter(|&&count| count > 0).count(); - let action_diversity = (unique_actions as f64 / 9.0) * 100.0; + let action_diversity = (unique_actions as f64 / 7.0) * 100.0; metrics.add_metric("action_diversity", action_diversity); - metrics.add_metric("action_space_size", 9.0); + metrics.add_metric("action_space_size", 7.0); let active_threshold = (total_exposure as f64 * 0.005).max(1.0); let active_count = total_action_counts.iter() .filter(|&&count| count as f64 >= active_threshold) .count(); - let active_diversity_pct = (active_count as f64 / 5.0) * 100.0; + let active_diversity_pct = (active_count as f64 / 7.0) * 100.0; metrics.add_metric("active_actions_count", active_count as f64); metrics.add_metric("active_diversity_pct", active_diversity_pct); @@ -158,13 +158,15 @@ impl DQNTrainer { metrics.add_metric("total_actions", total_actions as f64); - // Buy/sell/hold always from 5-exposure space (meaningful for P&L) - // 0=Short100, 1=Short50, 2=Flat, 3=Long50, 4=Long100 + // Buy/sell/hold from 7-exposure space (meaningful for P&L) + // 0=ShortSmall, 1=ShortHalf, 2=ShortFull, 3=Flat, 4=LongSmall, 5=LongHalf, 6=LongFull let sell_count: usize = total_action_counts.get(0).copied().unwrap_or(0) - + total_action_counts.get(1).copied().unwrap_or(0); - let hold_count: usize = total_action_counts.get(2).copied().unwrap_or(0); - let buy_count: usize = total_action_counts.get(3).copied().unwrap_or(0) - + total_action_counts.get(4).copied().unwrap_or(0); + + total_action_counts.get(1).copied().unwrap_or(0) + + total_action_counts.get(2).copied().unwrap_or(0); + let hold_count: usize = total_action_counts.get(3).copied().unwrap_or(0); + let buy_count: usize = total_action_counts.get(4).copied().unwrap_or(0) + + total_action_counts.get(5).copied().unwrap_or(0) + + total_action_counts.get(6).copied().unwrap_or(0); metrics.add_metric("buy_count", buy_count as f64); metrics.add_metric("sell_count", sell_count as f64); metrics.add_metric("hold_count", hold_count as f64); diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 4a2f45da0..13a72b011 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -72,8 +72,8 @@ impl DQNTrainer { let mut total_q_value = 0.0; let mut total_gradient_norm = 0.0; let mut total_reward = 0.0; - let mut total_action_counts = [0_usize; 9]; - let mut total_factored_action_counts = [0_usize; 81]; + let mut total_action_counts = [0_usize; 7]; + let mut total_factored_action_counts = [0_usize; 63]; self.log_training_config().await; @@ -400,7 +400,7 @@ impl DQNTrainer { } // M3: Q-value diagnostics — disabled, causes hang in single-threaded tokio - let q_diagnostics: Option<((f64, f64, f64), [f64; 9])> = None; + let q_diagnostics: Option<((f64, f64, f64), [f64; 7])> = None; // Three-Phase Training Pipeline let total_epochs = self.hyperparams.epochs; @@ -1536,9 +1536,9 @@ impl DQNTrainer { boundary: &Option, monitor: &mut TrainingMonitor, epoch_duration: std::time::Duration, - total_action_counts: &mut [usize; 9], - total_factored_action_counts: &mut [usize; 81], - mut q_diagnostics: Option<((f64, f64, f64), [f64; 9])>, + total_action_counts: &mut [usize; 7], + total_factored_action_counts: &mut [usize; 63], + mut q_diagnostics: Option<((f64, f64, f64), [f64; 7])>, ) -> Result { // Calculate epoch metrics (average over training steps) let (epoch_loss, epoch_q_value, epoch_gradient_norm) = match boundary { @@ -1881,15 +1881,15 @@ impl DQNTrainer { // With Flat-dominant policies, using total inflates the threshold and // mechanically kills diversity when Flat > 80%. The metric should measure // magnitude diversity WITHIN directional positions, not overall share. - let flat_count: usize = monitor.action_counts[3] + monitor.action_counts[4] + monitor.action_counts[5]; + let flat_count: usize = monitor.action_counts[3]; // Flat is index 3 only let directional_total = epoch_total.saturating_sub(flat_count).max(1); let active_threshold = (directional_total as f64 * 0.01).max(1.0); - // action_counts[9] tracks direction*magnitude combos (b0*b1 = 9) - // Flat cells (indices 3-5) always count if Flat has any actions + // action_counts[7] tracks exposure levels: 0-2=Short, 3=Flat, 4-6=Long + // Flat cell (index 3) always counts if Flat has any actions let active_dirmag = monitor.action_counts.iter().enumerate() .filter(|&(i, &c)| { - if (3..6).contains(&i) { - c > 0 // Flat cells: count if any actions + if i == 3 { + c > 0 // Flat cell: count if any actions } else { c as f64 >= active_threshold // Directional cells: threshold on directional total }