fix: monitoring 7→9 bin readback mismatch — diversity 15/81 → 81/81
The monitoring summary layout was shifted by 2 positions:
- Kernel writes: summary[5..14]=exp[9], [14..17]=order[3], [17..20]=urgency[3]
- Rust read (old): raw[5..12]=exp[7], raw[12..15]=order, raw[15..18]=urgency
- Rust read (new): raw[5..14]=exp[9], raw[14..17]=order, raw[17..20]=urgency
When num_actions changed 7→9, exp[7] and exp[8] became non-zero but Rust
read them as order_counts — the "order=3/3" was actually exp[7]+exp[8]+order[0].
Also fixed:
- MonitoringSummary.action_counts: [usize; 7] → [usize; 9]
- All downstream: monitoring.rs, metrics.rs, financials.rs, training_loop.rs
- Flat cell indices 3→{3,4,5} in diversity threshold
- exposure_names array expanded to 9 entries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,9 +12,9 @@ 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; 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!(
|
||||
|
||||
@@ -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<TrainingMetrics> {
|
||||
let final_loss = total_loss / num_epochs as f64;
|
||||
|
||||
@@ -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<EpochBoundaryMetrics>,
|
||||
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<EpochLogOutput> {
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user