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)
209 lines
6.3 KiB
Rust
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(())
|
|
}
|