Files
foxhunt/ml/tests/adaptive_temperature_test.rs
jgrusewski 00ef9e2866 Wave 15: Complete FactoredAction migration to 45-action system
Major Changes:
- Migrated from 3-action TradingAction to 45-action FactoredAction
- 45 actions: 5 exposure × 3 order types × 3 urgency levels
- Absolute exposure model (target positions -1.0 to +1.0)
- Transaction cost differentiation (Market 0.15%, LimitMaker 0.05%, IoC 0.10%)
- Fixed action diversity threshold (1.11% → 0.5% for 45-action space)

Bug Fixes:
- Bug #15: Incomplete FactoredAction integration (code existed but unused)
- Bug #16: Runtime crash in action diversity checking (hardcoded 3-action match)

Code Changes (13 files, ~464 lines):
- ml/src/dqn/action_space.rs: Core FactoredAction + 4 helper methods
- ml/src/trainers/dqn.rs: Action diversity refactored (3→45 dynamic)
- ml/src/dqn/reward.rs: calculate_reward() signature updated
- ml/src/dqn/portfolio_tracker.rs: execute_action() absolute exposure
- ml/src/dqn/dqn.rs: WorkingDQN action selection migrated
- ml/tests/*.rs: 9 test files updated with FactoredAction assertions

Test Results:
- 1-epoch smoke test: 100% action diversity (45/45 actions, 80.2s)
- 10-epoch production: 87.8% readiness (79/90 scorecard, 14.0 min)
- Loss convergence: 96.9% reduction (119K → 3.6K)
- Action diversity: 100% → 44% (healthy specialization)
- Checkpoint reliability: 12/12 files saved (100%)
- DQN tests: 195/195 passing (100%)
- ML baseline: 1,514/1,515 passing (99.93%)

Production Status:  CERTIFIED (87.8% readiness)
Go/No-Go:  GO FOR 100-EPOCH PRODUCTION TRAINING

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 23:27:02 +01:00

254 lines
8.2 KiB
Rust

//! Unit tests for Performance-Based Adaptive Temperature Decay
//!
//! Tests the adaptive temperature strategy where:
//! - Temperature decays faster when validation loss improves (>0.1% change)
//! - Temperature decays slower when validation loss plateaus (<0.1% change)
//! - Temperature increases when stuck in local optimum (10+ epochs without improvement)
use ml::dqn::{WorkingDQN, WorkingDQNConfig};
#[test]
fn test_temperature_adaptive_improving_loss() -> anyhow::Result<()> {
// Create DQN with adaptive temperature enabled
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = true;
config.loss_improvement_threshold = 0.999; // 0.1% improvement
config.plateau_window = 10;
config.temp_increase_factor = 1.05;
config.temperature_start = 1.0;
config.temperature_min = 0.1;
config.temperature_decay = 0.99; // Fast decay
config.temperature_slow_decay = 0.998; // Slow decay
let mut dqn = WorkingDQN::new(config)?;
// Simulate improving loss over 6 epochs (first is baseline)
let losses = vec![1.0, 0.95, 0.90, 0.85, 0.80, 0.75];
let initial_temp = dqn.get_temperature();
for loss in losses {
dqn.update_temperature_adaptive(loss);
}
// Temperature should decay faster (loss improved)
// First update establishes baseline, next 5 trigger fast decay
// Expected: temp * 0.99^5 ≈ 0.9509 (close to 0.95)
let final_temp = dqn.get_temperature();
assert!(
final_temp < initial_temp * 0.952, // Slightly relaxed for floating point
"Temperature should decay significantly when loss improves: {} -> {}",
initial_temp,
final_temp
);
// Plateau counter should be reset
assert_eq!(
dqn.get_plateau_count(),
0,
"Plateau count should reset on improvement"
);
Ok(())
}
#[test]
fn test_temperature_adaptive_plateaued_loss() -> anyhow::Result<()> {
// Create DQN with adaptive temperature enabled
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = true;
config.loss_improvement_threshold = 0.999; // 0.1% improvement
config.plateau_window = 10;
config.temp_increase_factor = 1.05;
config.temperature_start = 1.0;
config.temperature_min = 0.1;
config.temperature_decay = 0.99; // Fast decay
config.temperature_slow_decay = 0.998; // Slow decay
let mut dqn = WorkingDQN::new(config)?;
// Simulate plateaued loss (no improvement)
// First call establishes baseline, next 5 trigger plateau detection
let initial_temp = dqn.get_temperature();
for _ in 0..6 {
dqn.update_temperature_adaptive(1.0); // Same loss value
}
// Temperature should decay slowly (loss plateaued)
let final_temp = dqn.get_temperature();
assert!(
final_temp < initial_temp,
"Temperature should decay slowly when loss plateaus: {} -> {}",
initial_temp,
final_temp
);
assert!(
final_temp > initial_temp * 0.98,
"Slow decay should be minimal: {} -> {}",
initial_temp,
final_temp
);
// Plateau counter should increase (5 epochs after baseline)
assert_eq!(
dqn.get_plateau_count(),
5,
"Plateau count should track epochs without improvement"
);
Ok(())
}
#[test]
fn test_temperature_adaptive_stuck_recovery() -> anyhow::Result<()> {
// Create DQN with adaptive temperature enabled
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = true;
config.loss_improvement_threshold = 0.999; // 0.1% improvement
config.plateau_window = 10;
config.temp_increase_factor = 1.05;
config.temperature_start = 1.0;
config.temperature_min = 0.1;
config.temperature_decay = 0.99; // Fast decay
config.temperature_slow_decay = 0.998; // Slow decay
let mut dqn = WorkingDQN::new(config)?;
// Simulate stuck in local optimum (13 epochs: 1 baseline + 12 plateau)
// After 11 plateau epochs (window=10), temperature should increase and reset counter
let initial_temp = dqn.get_temperature();
for _ in 0..13 {
dqn.update_temperature_adaptive(1.0); // Same loss value
}
// Temperature should increase (escape local optimum)
// First update: baseline (temp=1.0)
// Next 10 updates: slow decay (temp = 1.0 * 0.998^10 ≈ 0.980)
// Update 12: triggers increase (temp = 0.980 * 1.05 ≈ 1.029, plateau resets)
// Update 13: slow decay again (temp = 1.029 * 0.998 ≈ 1.027)
let final_temp = dqn.get_temperature();
assert!(
final_temp > initial_temp * 1.02, // Should be ~1.027 (relaxed for floating point)
"Temperature should increase when stuck: {} -> {} (expected >{})",
initial_temp,
final_temp,
initial_temp * 1.02
);
// Plateau counter should reset after temperature increase
// 13 total - 1 baseline - 11 that triggered increase = 1 remaining
assert_eq!(
dqn.get_plateau_count(),
1,
"Plateau count should reset after temperature increase"
);
Ok(())
}
#[test]
fn test_temperature_adaptive_bounds() -> anyhow::Result<()> {
// Create DQN with adaptive temperature enabled
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = true;
config.temperature_start = 1.0;
config.temperature_min = 0.1;
config.temperature_decay = 0.99;
let mut dqn = WorkingDQN::new(config)?;
// Decay temperature many times
for _ in 0..1000 {
dqn.update_temperature_adaptive(0.5); // Improving loss
}
// Temperature should not go below minimum
assert!(
dqn.get_temperature() >= 0.1,
"Temperature should respect minimum bound: {}",
dqn.get_temperature()
);
// Try to increase temperature many times
for _ in 0..100 {
// Simulate stuck condition (plateau_window reached)
for _ in 0..11 {
dqn.update_temperature_adaptive(1.0); // Same loss
}
}
// Temperature should not exceed start value
assert!(
dqn.get_temperature() <= 1.0,
"Temperature should not exceed start value: {}",
dqn.get_temperature()
);
Ok(())
}
#[test]
fn test_temperature_adaptive_disabled() -> anyhow::Result<()> {
// Create DQN with adaptive temperature DISABLED
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = false; // Disabled
config.temperature_start = 1.0;
config.temperature_decay = 0.995;
let mut dqn = WorkingDQN::new(config)?;
// Simulate improving loss
let initial_temp = dqn.get_temperature();
for _ in 0..5 {
dqn.update_temperature_adaptive(0.5); // Improving loss
}
// Temperature should use fixed decay (not adaptive)
let expected_temp = initial_temp * 0.995_f64.powi(5);
let actual_temp = dqn.get_temperature();
assert!(
(actual_temp - expected_temp).abs() < 0.001,
"Fixed decay should be used when adaptive disabled: {} vs {}",
actual_temp,
expected_temp
);
Ok(())
}
#[test]
fn test_temperature_adaptive_loss_window_averaging() -> anyhow::Result<()> {
// Create DQN with adaptive temperature enabled
let mut config = WorkingDQNConfig::emergency_safe_defaults();
config.use_adaptive_temperature = true;
config.loss_improvement_threshold = 0.999; // 0.1% improvement
config.plateau_window = 10;
let mut dqn = WorkingDQN::new(config)?;
// Simulate noisy loss (oscillating but overall improving)
let noisy_losses = vec![1.0, 1.1, 0.9, 1.0, 0.8, 0.9, 0.7, 0.85, 0.65, 0.8];
let initial_temp = dqn.get_temperature();
for loss in noisy_losses {
dqn.update_temperature_adaptive(loss);
}
// Temperature should decay (loss averaged over window shows improvement)
let final_temp = dqn.get_temperature();
assert!(
final_temp < initial_temp,
"Temperature should decay despite noisy loss: {} -> {}",
initial_temp,
final_temp
);
// Plateau counter should be low (averaging smooths noise)
assert!(
dqn.get_plateau_count() < 5,
"Plateau count should be low with noisy but improving loss: {}",
dqn.get_plateau_count()
);
Ok(())
}