diff --git a/crates/ml/src/cuda_pipeline/gpu_monitoring.rs b/crates/ml/src/cuda_pipeline/gpu_monitoring.rs index 279359adc..dea59f61f 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 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 dir×mag level counts (9 bins: dir(3) × mag(3), e.g. ShortSmall=0 .. LongFull=8). + pub action_counts: [usize; 9], /// Per-order-type counts (3: Market, LimitMaker, IoC). pub order_counts: [usize; 3], /// Per-urgency counts (3: Patient, Normal, Aggressive). @@ -110,15 +110,17 @@ impl GpuMonitoringReducer { min_reward: raw[2], max_reward: raw[3], sharpe_estimate: raw[4], + // Kernel layout: summary[5..14]=exp[9], summary[14..17]=order[3], + // summary[17..20]=urgency[3], summary[20]=total, summary[21]=trades 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[11] as usize, raw[12] as usize, raw[13] 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, + 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, }) } } @@ -131,7 +133,7 @@ mod tests { fn test_monitoring_summary_default() { let s = MonitoringSummary::default(); assert_eq!(s.total_experiences, 0); - assert_eq!(s.action_counts, [0; 7]); + assert_eq!(s.action_counts, [0; 9]); } #[test] diff --git a/crates/ml/src/trainers/dqn/financials.rs b/crates/ml/src/trainers/dqn/financials.rs index 89ef153a1..9f0b6c77b 100644 --- a/crates/ml/src/trainers/dqn/financials.rs +++ b/crates/ml/src/trainers/dqn/financials.rs @@ -29,7 +29,7 @@ pub(crate) struct EpochFinancials { /// 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; 7], + action_counts: &[usize; 9], initial_capital: f64, bars_per_day: f64, ) -> EpochFinancials { @@ -203,7 +203,7 @@ mod tests { #[test] fn test_empty_trade_stats() { let ts = TradeStats::default(); - let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 9], 100_000.0, 390.0); assert_eq!(f.total_trades, 0); assert_eq!(f.sharpe, 0.0); } @@ -221,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; 7], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 9], 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); @@ -242,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; 7], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 9], 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); @@ -253,7 +253,7 @@ mod tests { #[test] fn test_action_distribution_7_actions() { - let mut actions = [0usize; 7]; + let mut actions = [0usize; 9]; actions[0] = 10; // ShortSmall -> SELL actions[1] = 10; // ShortHalf -> SELL actions[3] = 20; // Flat -> HOLD @@ -279,7 +279,7 @@ mod tests { #[test] fn test_action_distribution_all_indices_counted() { - let mut actions = [0usize; 7]; + let mut actions = [0usize; 9]; for i in 0..7 { actions[i] = 10; } let ts = TradeStats { total_trades: 1, @@ -324,7 +324,7 @@ mod tests { step_returns, done_flags, }; - let f = compute_epoch_financials(&ts, &[0; 7], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 9], 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. @@ -356,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; 7], 100_000.0, 390.0); + let f = compute_epoch_financials(&ts, &[0; 9], 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 bc5c81cc4..f6ecfcc92 100644 --- a/crates/ml/src/trainers/dqn/monitoring.rs +++ b/crates/ml/src/trainers/dqn/monitoring.rs @@ -12,9 +12,9 @@ use crate::dqn::action_space::FactoredAction; pub(crate) struct TrainingMonitor { pub(crate) epoch: usize, pub(crate) reward_history: Vec, - pub(crate) action_counts: [usize; 7], // 7 exposure levels (ShortSmall..LongFull) + pub(crate) action_counts: [usize; 9], // 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) q_value_counts: [usize; 9], // 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: 7 exposure * 3 order * 3 urgency = 63 actions. @@ -40,9 +40,9 @@ impl TrainingMonitor { Self { epoch, reward_history: Vec::new(), - action_counts: [0; 7], + action_counts: [0; 9], q_value_sums: [0.0; 7], - q_value_counts: [0; 7], + q_value_counts: [0; 9], order_type_counts: [0; 3], urgency_counts: [0; 3], factored_action_counts: [0; 63], @@ -203,7 +203,7 @@ impl TrainingMonitor { return Ok(()); } - let exposure_names = ["S_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "F_Small", "F_Half", "F_Full", "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 { @@ -246,7 +246,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_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "F_Small", "F_Half", "F_Full", "L_Small", "L_Half", "L_Full"]; warn!( "Q-VALUE DIVERGENCE at epoch {}: {}", self.epoch, @@ -265,7 +265,7 @@ impl TrainingMonitor { if self.epoch % 10 == 0 { let total_actions: usize = self.action_counts.iter().sum(); if total_actions > 0 { - let exposure_names = ["S_Small", "S_Half", "S_Full", "Flat", "L_Small", "L_Half", "L_Full"]; + let exposure_names = ["S_Small", "S_Half", "S_Full", "F_Small", "F_Half", "F_Full", "L_Small", "L_Half", "L_Full"]; // Log all 7 exposure level actions debug!( diff --git a/crates/ml/src/trainers/dqn/trainer/metrics.rs b/crates/ml/src/trainers/dqn/trainer/metrics.rs index 93b547543..83fc515d3 100644 --- a/crates/ml/src/trainers/dqn/trainer/metrics.rs +++ b/crates/ml/src/trainers/dqn/trainer/metrics.rs @@ -42,7 +42,7 @@ impl DQNTrainer { num_epochs: usize, training_duration: std::time::Duration, early_stopped: bool, - total_action_counts: [usize; 7], // 7 exposure levels + total_action_counts: [usize; 9], // 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; diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index e15146e21..665dc085e 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -72,7 +72,7 @@ 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; 7]; + let mut total_action_counts = [0_usize; 9]; let mut total_factored_action_counts = [0_usize; 63]; self.log_training_config().await; @@ -1506,7 +1506,7 @@ impl DQNTrainer { boundary: &Option, monitor: &mut TrainingMonitor, epoch_duration: std::time::Duration, - total_action_counts: &mut [usize; 7], + total_action_counts: &mut [usize; 9], total_factored_action_counts: &mut [usize; 63], mut q_diagnostics: Option<((f64, f64, f64), [f64; 7])>, ) -> Result { @@ -1851,17 +1851,18 @@ 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]; // Flat is index 3 only + // 9-bin layout: dir(3)×mag(3). Flat cells = indices 3,4,5 (dir=1 × mag=0,1,2). + // In practice only Flat×Half (index 4) is reachable (Flat forces mag=1). + let flat_count: usize = monitor.action_counts[3] + monitor.action_counts[4] + monitor.action_counts[5]; 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[7] tracks exposure levels: 0-2=Short, 3=Flat, 4-6=Long - // Flat cell (index 3) always counts if Flat has any actions + // action_counts[9]: dir×mag combos. Flat cells (3,4,5) always count if non-zero. let active_dirmag = monitor.action_counts.iter().enumerate() .filter(|&(i, &c)| { - if i == 3 { - c > 0 // Flat cell: count if any actions + if (3..=5).contains(&i) { + c > 0 // Flat cells: count if any actions } else { - c as f64 >= active_threshold // Directional cells: threshold on directional total + c as f64 >= active_threshold // Directional cells: threshold } }).count(); let active_ord = monitor.order_type_counts.iter() @@ -1871,9 +1872,9 @@ impl DQNTrainer { let active_factored = active_dirmag * active_ord * active_urg; let diversity_pct = (active_factored as f64 / total_factored_space as f64) * 100.0; - // Max reachable dir*mag = (b0-1)*b1 + 1: Flat forces mag=Half, so only - // 1 Flat cell is reachable out of b1. Short and Long each have b1 cells. - let max_dirmag = (b0.saturating_sub(1)) * b1 + 1; // 2*3+1 = 7 + // Max reachable dir*mag = (b0-1)*b1 + 1: Short and Long each have b1 mag options. + // Flat forces mag=Half, so only 1 of the 3 Flat cells is reachable. + let max_dirmag = (b0.saturating_sub(1)) * b1 + 1; // 2*3+1 = 7 (of 9 total) info!( "Epoch {}/{}: Action diversity={}/{} ({:.1}%) — dir*mag={}/{} order={}/{} urgency={}/{}", epoch + 1, self.hyperparams.epochs,