Files
foxhunt/ml/tests/entropy_integration_test.rs
jgrusewski f5947c2b22 Wave 16S-V11: Bug #8 fix + P2-A/B implementation
Bug #8 (CRITICAL): Fixed action selection frequency catastrophe
- Root cause: execute_action called during training (522,713 orders/epoch)
- Fix: Removed execute_action from experience collection loop (line 928-936)
- Impact: 522,713 → 0 orders/epoch (100% reduction)
- Transaction costs: $338K → $0 (eliminated)
- Test suite: ml/tests/action_selection_frequency_test.rs (3/3 passing)

P2-A: Configurable Initial Capital
- CLI argument: --initial-capital (default: $100K, min: $1K)
- Files modified: trainers/dqn.rs, train_dqn.rs, hyperopt adapter
- Test suite: ml/tests/configurable_capital_test.rs (8/8 passing)
- Supports: Small accounts ($10K), Standard ($100K), Institutional ($500K+)

P2-B: Cash Reserve Requirement
- CLI argument: --cash-reserve-percent (default: 0%, range: 0-100%)
- Reserve enforcement: BUY trades only (SELL always allowed)
- Dynamic reserve adjusts with portfolio value
- Files modified: portfolio_tracker.rs (70 lines), trainers/dqn.rs, train_dqn.rs
- Test suite: ml/tests/cash_reserve_requirement_test.rs (10/10 passing)

Test Status: 21/21 core tests passing (P2-C deferred due to API mismatch)

Wave 16S-V11 Agents:
- Agent #1: Bug #8 investigation (transaction cost analysis)
- Agent #2: P2-A implementation (configurable capital)
- Agent #3: P2-B implementation + test fix (cash reserve)
- Agent #4: Integration validation (certification report)
2025-11-12 23:05:51 +01:00

209 lines
6.3 KiB
Rust

//! Integration tests for entropy regularization in DQN training
//!
//! These tests verify that entropy regularization is properly integrated
//! into the DQN loss calculation and maintains action diversity.
use candle_core::{DType, Device, Tensor};
use ml::dqn::{EntropyRegularizer, Experience, FactoredAction, WorkingDQN, WorkingDQNConfig};
use ml::MLError;
/// Test that entropy regularizer calculates correct penalties for 45-action space
#[test]
fn test_entropy_regularizer_45_actions() -> Result<(), MLError> {
let regularizer = EntropyRegularizer::new();
// Create Q-values for 45 actions (batch_size=1)
let device = Device::cuda_if_available(0)?;
// Case 1: Uniform distribution (high entropy, should get bonus)
let uniform_q = vec![1.0f32; 45];
let q_tensor = Tensor::from_vec(uniform_q, (1, 45), &device)?;
let bonus = regularizer.calculate_entropy_bonus(&q_tensor)?;
// Uniform distribution: entropy = log(45) ≈ 3.807, normalized = 1.0
// Expected: 1.0 > 0.7, so bonus = 1.0 * 2.0 = 2.0
assert!(
bonus > 1.8,
"Uniform distribution should get bonus > 1.8, got {}",
bonus
);
// Case 2: Deterministic policy (low entropy, should get penalty)
let mut deterministic_q = vec![0.0f32; 45];
deterministic_q[0] = 10.0; // Only action 0 has high Q-value
let q_tensor = Tensor::from_vec(deterministic_q, (1, 45), &device)?;
let penalty = regularizer.calculate_entropy_bonus(&q_tensor)?;
// Deterministic: entropy ≈ 0, normalized ≈ 0
// Expected: 0 < 0.7, so penalty = -(0.7 - 0) * 3.0 = -2.1
assert!(
penalty < -2.0,
"Deterministic policy should get penalty < -2.0, got {}",
penalty
);
// Case 3: High diversity (all 45 actions roughly equal)
let high_diversity_q: Vec<f32> = (0..45).map(|i| 5.0 - (i as f32 * 0.05)).collect();
let q_tensor = Tensor::from_vec(high_diversity_q, (1, 45), &device)?;
let result = regularizer.calculate_entropy_bonus(&q_tensor)?;
// High diversity should get bonus (normalized entropy > 0.7)
assert!(
result > 0.0,
"High diversity should get bonus > 0, got {}",
result
);
Ok(())
}
/// Test that DQN calculate_entropy_penalty uses 45-action space
#[test]
fn test_dqn_entropy_penalty_45_actions() -> Result<(), MLError> {
let config = WorkingDQNConfig {
state_dim: 128,
num_actions: 45,
hidden_dims: vec![256, 128],
learning_rate: 1e-4,
gamma: 0.99,
epsilon_start: 1.0,
epsilon_end: 0.01,
epsilon_decay: 0.995,
replay_buffer_capacity: 10000,
batch_size: 32,
min_replay_size: 100,
target_update_freq: 100,
use_double_dqn: true,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: false,
warmup_steps: 0,
entropy_coefficient: 0.01,
};
let mut dqn = WorkingDQN::new(config)?;
// Simulate taking diverse actions (all 45 actions once)
let dummy_state = vec![0.0f32; 128];
for action_idx in 0..45 {
let action = FactoredAction::from_index(action_idx)?;
let reward = 0.1;
let next_state = vec![0.0f32; 128];
let done = false;
// Track action for entropy calculation
dqn.track_action(action);
// Store experience in replay buffer
let exp = Experience::new(
dummy_state.clone(),
action.to_index() as u8,
reward,
next_state,
done,
);
dqn.store_experience(exp)?;
}
// Calculate entropy - should be high (all 45 actions used equally)
let entropy = dqn.get_action_entropy();
// Max entropy for 45 actions: log2(45) ≈ 5.492
// With uniform distribution: entropy = 5.492
assert!(
entropy > 5.4,
"Uniform 45-action distribution should have entropy > 5.4, got {}",
entropy
);
Ok(())
}
/// Test that get_action_entropy correctly counts 45 actions
#[test]
fn test_get_action_entropy_45_actions() -> Result<(), MLError> {
let config = WorkingDQNConfig {
state_dim: 128,
num_actions: 45,
hidden_dims: vec![256, 128],
learning_rate: 1e-4,
gamma: 0.99,
epsilon_start: 1.0,
epsilon_end: 0.01,
epsilon_decay: 0.995,
replay_buffer_capacity: 10000,
batch_size: 32,
min_replay_size: 100,
target_update_freq: 100,
use_double_dqn: true,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: false,
warmup_steps: 0,
entropy_coefficient: 0.01,
};
let mut dqn = WorkingDQN::new(config)?;
// Test 1: All same action (deterministic)
let dummy_state = vec![0.0f32; 128];
for _ in 0..100 {
let action = FactoredAction::from_index(0)?;
dqn.track_action(action);
}
let entropy_deterministic = dqn.get_action_entropy();
assert!(
entropy_deterministic < 0.1,
"Deterministic policy should have entropy ≈ 0, got {}",
entropy_deterministic
);
// Test 2: Uniform distribution (all 45 actions)
let mut dqn = WorkingDQN::new(WorkingDQNConfig {
state_dim: 128,
num_actions: 45,
hidden_dims: vec![256, 128],
learning_rate: 1e-4,
gamma: 0.99,
epsilon_start: 1.0,
epsilon_end: 0.01,
epsilon_decay: 0.995,
replay_buffer_capacity: 10000,
batch_size: 32,
min_replay_size: 100,
target_update_freq: 100,
use_double_dqn: true,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: false,
warmup_steps: 0,
entropy_coefficient: 0.01,
})?;
for action_idx in 0..45 {
let action = FactoredAction::from_index(action_idx)?;
dqn.track_action(action);
}
let entropy_uniform = dqn.get_action_entropy();
// Max entropy for 45 actions: log2(45) ≈ 5.492
assert!(
entropy_uniform > 5.4,
"Uniform distribution should have entropy ≈ 5.492, got {}",
entropy_uniform
);
Ok(())
}