feat: monitoring + metrics arrays 5→9, factored 45→81

Updated all fixed-size arrays across monitoring.rs, financials.rs,
metrics.rs, training_loop.rs from [_;5]→[_;9] and [_;45]→[_;81].
DIRECTION_LUT expanded to 9 levels (-1.0 to +1.0 in 0.25 steps).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-23 09:41:14 +01:00
parent 51037f60b8
commit 889d9263f4
4 changed files with 26 additions and 26 deletions

View File

@@ -26,7 +26,7 @@ pub(crate) struct EpochFinancials {
/// - `initial_capital`: starting equity for return calculation (default 100_000)
pub(crate) fn compute_epoch_financials(
pnl_history: &VecDeque<f64>,
action_counts: &[usize; 5],
action_counts: &[usize; 9],
initial_capital: f64,
) -> EpochFinancials {
if pnl_history.is_empty() {
@@ -135,7 +135,7 @@ mod tests {
#[test]
fn test_empty_history() {
let f = compute_epoch_financials(&VecDeque::new(), &[0; 5], 100_000.0);
let f = compute_epoch_financials(&VecDeque::new(), &[0; 9], 100_000.0);
assert_eq!(f.total_trades, 0);
assert_eq!(f.sharpe, 0.0);
}
@@ -143,7 +143,7 @@ mod tests {
#[test]
fn test_all_winning() {
let pnl: VecDeque<f64> = vec![10.0, 20.0, 30.0, 15.0, 25.0].into();
let f = compute_epoch_financials(&pnl, &[0; 5], 100_000.0);
let f = compute_epoch_financials(&pnl, &[0; 9], 100_000.0);
assert_eq!(f.win_rate, 1.0);
assert_eq!(f.total_trades, 5);
assert!(f.sharpe > 0.0);
@@ -154,7 +154,7 @@ mod tests {
#[test]
fn test_mixed_pnl() {
let pnl: VecDeque<f64> = vec![100.0, -50.0, 75.0, -25.0, 50.0].into();
let f = compute_epoch_financials(&pnl, &[0; 5], 100_000.0);
let f = compute_epoch_financials(&pnl, &[0; 9], 100_000.0);
assert_eq!(f.total_trades, 5);
assert!((f.win_rate - 0.6).abs() < 1e-10);
assert!(f.total_return > 0.0);
@@ -165,7 +165,7 @@ mod tests {
#[test]
fn test_action_distribution() {
let mut actions = [0usize; 5];
let mut actions = [0usize; 9];
actions[4] = 100; // Long100 → BUY
actions[0] = 50; // Short100 → SELL
actions[2] = 50; // Flat → HOLD

View File

@@ -12,14 +12,14 @@ use crate::dqn::action_space::FactoredAction;
pub(crate) struct TrainingMonitor {
pub(crate) epoch: usize,
pub(crate) reward_history: Vec<f32>,
pub(crate) action_counts: [usize; 5], // 5 exposure levels (DQN action space)
pub(crate) q_value_sums: [f64; 5], // Sum of Q-values per exposure action
pub(crate) q_value_counts: [usize; 5], // Count of Q-values per exposure action
pub(crate) action_counts: [usize; 9], // 5 exposure levels (DQN action space)
pub(crate) q_value_sums: [f64; 9], // Sum of Q-values per exposure action
pub(crate) q_value_counts: [usize; 9], // Count of Q-values per exposure action
pub(crate) order_type_counts: [usize; 3], // Market, LimitMaker, IoC
pub(crate) urgency_counts: [usize; 3], // Patient, Normal, Aggressive
/// Factored action counts: 5 exposure × 3 order × 3 urgency = 45 actions.
/// Index = exposure * 9 + order * 3 + urgency (0-44).
pub(crate) factored_action_counts: [usize; 45],
pub(crate) factored_action_counts: [usize; 81],
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; 5],
q_value_sums: [0.0; 5],
q_value_counts: [0; 5],
action_counts: [0; 9],
q_value_sums: [0.0; 9],
q_value_counts: [0; 9],
order_type_counts: [0; 3],
urgency_counts: [0; 3],
factored_action_counts: [0; 45],
factored_action_counts: [0; 81],
consecutive_constant_epochs: 0,
q_value_min: f64::INFINITY,
q_value_max: f64::NEG_INFINITY,

View File

@@ -42,8 +42,8 @@ impl DQNTrainer {
num_epochs: usize,
training_duration: std::time::Duration,
early_stopped: bool,
total_action_counts: [usize; 5], // 5 exposure levels
total_factored_action_counts: [usize; 45], // 45 factored actions
total_action_counts: [usize; 9], // 5 exposure levels
total_factored_action_counts: [usize; 81], // 45 factored actions
) -> Result<TrainingMetrics> {
let final_loss = total_loss / num_epochs as f64;
let avg_q_value_final = total_q_value / num_epochs as f64;
@@ -189,10 +189,10 @@ impl DQNTrainer {
///
/// Returns (gap_stats, per_action_avgs) where:
/// - gap_stats: (mean_gap, min_gap, max_gap) of Q_best - Q_second_best
/// - per_action_avgs: `[f64; 5]` averages (one per exposure action, capped at 5)
/// - per_action_avgs: `[f64; 9]` averages (one per exposure action, capped at 5)
pub(crate) async fn compute_epoch_q_diagnostics(&mut self) -> Option<(
(f64, f64, f64),
[f64; 5],
[f64; 9],
)> {
let agent = self.agent.read().await;
let buffer = agent.memory();
@@ -236,7 +236,7 @@ impl DQNTrainer {
let cols = total_actions;
let rows = sample_size;
let mut gaps = Vec::with_capacity(rows);
let mut col_sums = [0.0_f64; 5];
let mut col_sums = [0.0_f64; 9];
for r in 0..rows {
let row_offset = r * cols;
let mut best_val = f32::NEG_INFINITY;
@@ -272,7 +272,7 @@ impl DQNTrainer {
let gap_mean = gaps.iter().sum::<f32>() / rows as f32;
let gap_min = gaps.iter().copied().fold(f32::INFINITY, f32::min);
let gap_max = gaps.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut per_action_avgs = [0.0_f64; 5];
let mut per_action_avgs = [0.0_f64; 9];
for (i, avg) in per_action_avgs.iter_mut().enumerate() {
*avg = col_sums.get(i).copied().unwrap_or(0.0) / rows_f64;
}
@@ -483,8 +483,8 @@ impl DQNTrainer {
// ── CPU greedy action selection ──
// Direction LUT maps exposure head index → position size direction.
// 0=Short100(-1.0), 1=Short50(-0.5), 2=Flat(0.0), 3=Long50(0.5), 4=Long100(1.0)
const DIRECTION_LUT: [f32; 5] = [-1.0, -0.5, 0.0, 0.5, 1.0];
let use_branching = self.hyperparams.use_branching && total_actions >= 11;
const DIRECTION_LUT: [f32; 9] = [-1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0];
let use_branching = self.hyperparams.use_branching && total_actions >= 15;
let rewards: Vec<f32> = (0..sample_size).map(|i| {
let row_start = i * total_actions;
@@ -507,7 +507,7 @@ impl DQNTrainer {
.map(|(j, _)| j.min(4))
.unwrap_or(2) // default: Flat
};
let direction = DIRECTION_LUT[exposure_idx.min(4)];
let direction = DIRECTION_LUT[exposure_idx.min(8)];
// PnL-based reward: direction × (next_close - cur_close) / cur_close
let cur = current_closes.get(i).copied().unwrap_or(0.0);

View File

@@ -70,8 +70,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; 5];
let mut total_factored_action_counts = [0_usize; 45];
let mut total_action_counts = [0_usize; 9];
let mut total_factored_action_counts = [0_usize; 81];
self.log_training_config().await;
@@ -1446,8 +1446,8 @@ impl DQNTrainer {
boundary: &Option<EpochBoundaryMetrics>,
monitor: &mut TrainingMonitor,
epoch_duration: std::time::Duration,
total_action_counts: &mut [usize; 5],
total_factored_action_counts: &mut [usize; 45],
total_action_counts: &mut [usize; 9],
total_factored_action_counts: &mut [usize; 81],
) -> Result<EpochLogOutput> {
// Calculate epoch metrics (average over training steps)
let (epoch_loss, epoch_q_value, epoch_gradient_norm) = match boundary {