Files
foxhunt/crates/ml/tests/training_edge_cases.rs
jgrusewski c0c44a5f17 feat(dqn): GPU-native regime classification with 42-dim feature vector
Expand FeatureVector from 40 to 42 dimensions by including ADX(14) at
index 40 and CUSUM direction at index 41 from the existing CPU feature
extraction pipeline. This eliminates proxy-based regime classification
and enables GPU-native regime detection via tensor narrow/comparison ops.

Key changes:
- extraction.rs: wire RegimeADXFeatures + RegimeCUSUMFeatures into
  extract_current_features_v2(), output 42 features per bar
- regime_conditional.rs: classify_regime_masks_gpu() creates per-regime
  mask tensors entirely on GPU (ADX > 0.25 = trending, |CUSUM| > 0.7 =
  volatile, else ranging). Zero CPU roundtrip in training hot path.
- trainer.rs/config.rs: state_dim 43→45 (no OFI), 51→53 (with OFI),
  aligned dims unchanged (48/56). GPU batch insertion for all 3 heads.
- CUDA header: MARKET_DIM 40→42
- walk_forward.rs: FEATURE_DIM 40→42
- 42 files updated, all [f64;40]→[f64;42] propagated across workspace

Test results: ml=874/0, ml-dqn=354/0, ml-features=282/0, ml-core=274/0
Real data GPU smoke tests: 7/7 passed (OHLCV + OFI + trade enrichment)
Hyperopt baseline RL: 2 trials completed on local RTX 3050 Ti

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:16:05 +01:00

935 lines
28 KiB
Rust

//! Comprehensive edge case tests for ML model training loops
//!
//! This module tests edge cases across DQN, PPO, and Liquid Neural Networks:
//! - Training iteration with NaN/Inf loss values
//! - Gradient explosion/vanishing scenarios
//! - Convergence check boundary conditions
//! - Batch size edge cases (size=1, size=max)
//! - Learning rate edge cases (zero, negative, very large)
//! - Training loop early stopping conditions
//! - Model state save/load during training
//!
//! Target: +20% coverage increase for ml crate
#![allow(unused_crate_dependencies)]
use common::trading::MarketRegime;
use ml::dqn::agent::{DQNAgent, TradingAction};
use ml_core::common::action::{ExposureLevel, FactoredAction, OrderType, Urgency};
use ml::dqn::DQNConfig;
use ml::dqn::experience::Experience;
use ml::liquid::network::{LiquidNetwork, OutputLayerConfig};
use ml::liquid::training::{LiquidTrainer, LiquidTrainingConfig, TrainingBatch, TrainingSample};
use ml::liquid::{ActivationType, FixedPoint, LiquidNetworkConfig, NetworkType, PRECISION};
use ml::ppo::ppo::{PPOConfig, PPO};
use ml::ppo::trajectories::{Trajectory, TrajectoryBatch, TrajectoryStep};
use ml::MLError;
// ============================================================================
// DQN Training Edge Cases
// ============================================================================
#[tokio::test]
async fn test_dqn_training_with_insufficient_experiences() -> Result<(), Box<dyn std::error::Error>>
{
let config = DQNConfig {
batch_size: 32,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add fewer experiences than batch size
for i in 0..10 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Hold.to_int(),
1.0,
vec![i as f32 + 0.1; 52],
false,
);
agent.store_experience(experience)?;
}
// Training should fail with insufficient experiences
let result = agent.train();
assert!(result.is_err());
match result {
Err(MLError::TrainingError(msg)) => {
assert!(msg.contains("Not enough experiences"));
},
_ => panic!("Expected TrainingError for insufficient experiences"),
}
Ok(())
}
#[tokio::test]
async fn test_dqn_training_with_batch_size_one() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
state_dim: 52,
batch_size: 1,
replay_buffer_capacity: 1000,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add minimum experiences
for i in 0..20 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Buy.to_int(),
(i as f32) * 0.1,
vec![i as f32 + 0.5; 52],
i % 5 == 0,
);
agent.store_experience(experience)?;
}
// Training with batch size 1 should succeed
let loss = agent.train()?;
assert!(loss.is_finite());
assert!(loss >= 0.0);
Ok(())
}
#[tokio::test]
async fn test_dqn_training_with_large_batch_size() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
state_dim: 52,
batch_size: 512,
replay_buffer_capacity: 10_000,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add enough experiences for large batch
for i in 0..1000 {
let experience = Experience::new(
vec![(i % 100) as f32; 52],
TradingAction::from_int((i % 3) as u8).unwrap().to_int(),
((i % 20) as f32) - 10.0,
vec![((i + 1) % 100) as f32; 52],
i % 50 == 0,
);
agent.store_experience(experience)?;
}
// Training with large batch should succeed
let loss = agent.train()?;
assert!(loss.is_finite());
Ok(())
}
#[tokio::test]
async fn test_dqn_training_with_extreme_rewards() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig::default();
let mut agent = DQNAgent::new(config)?;
// Add experiences with extreme reward values
let extreme_rewards = vec![f32::MAX / 1000.0, -f32::MAX / 1000.0, 1e6, -1e6, 0.0];
for (i, &reward) in extreme_rewards.iter().enumerate() {
for j in 0..100 {
let experience = Experience::new(
vec![(i * 100 + j) as f32; 52],
TradingAction::Hold.to_int(),
reward,
vec![(i * 100 + j + 1) as f32; 52],
false,
);
agent.store_experience(experience)?;
}
}
// Training should handle extreme rewards gracefully
let loss = agent.train()?;
assert!(
loss.is_finite(),
"Loss should be finite with extreme rewards"
);
Ok(())
}
#[tokio::test]
async fn test_dqn_learning_rate_zero() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
learning_rate: 0.0,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add experiences
for i in 0..400 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Buy.to_int(),
1.0,
vec![i as f32 + 0.1; 52],
false,
);
agent.store_experience(experience)?;
}
// Training with zero learning rate should succeed but not change weights
let loss_1 = agent.train()?;
let loss_2 = agent.train()?;
// Loss should remain similar (within 10% due to sampling randomness)
assert!((loss_1 - loss_2).abs() / loss_1 < 0.1);
Ok(())
}
#[tokio::test]
async fn test_dqn_learning_rate_very_large() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
learning_rate: 10.0, // Very large learning rate
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add experiences
for i in 0..400 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Sell.to_int(),
(i % 10) as f32,
vec![i as f32 + 0.1; 52],
i % 100 == 0,
);
agent.store_experience(experience)?;
}
// Training with very large learning rate should still produce finite loss
let loss = agent.train()?;
assert!(
loss.is_finite(),
"Loss should be finite even with large learning rate"
);
Ok(())
}
#[tokio::test]
async fn test_dqn_learning_rate_decay() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
learning_rate: 0.001,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
let initial_lr = agent.get_learning_rate();
// Apply learning rate decay
agent.update_learning_rate(0.5)?;
let new_lr = agent.get_learning_rate();
assert_eq!(new_lr, initial_lr * 0.5);
// Apply multiple decays
agent.update_learning_rate(0.1)?;
let final_lr = agent.get_learning_rate();
assert_eq!(final_lr, initial_lr * 0.5 * 0.1);
Ok(())
}
#[tokio::test]
async fn test_dqn_target_network_update_frequency() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig {
target_update_freq: 5,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add experiences
for i in 0..500 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Hold.to_int(),
1.0,
vec![i as f32 + 0.1; 52],
false,
);
agent.store_experience(experience)?;
}
// Train multiple times to trigger target network update
for _ in 0..10 {
let loss = agent.train()?;
assert!(loss.is_finite());
}
Ok(())
}
#[tokio::test]
async fn test_dqn_checkpoint_save_load_during_training() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig::default();
let mut agent = DQNAgent::new(config)?;
// Add experiences and train
for i in 0..400 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Buy.to_int(),
(i % 10) as f32,
vec![i as f32 + 0.1; 52],
false,
);
agent.store_experience(experience)?;
}
agent.train()?;
let metrics_before = agent.get_metrics().clone();
// Save checkpoint
let temp_dir = std::env::temp_dir();
let checkpoint_path = temp_dir.join("dqn_training_checkpoint.json");
agent.save_checkpoint(&checkpoint_path)?;
// Create new agent and load checkpoint
let config = DQNConfig::default();
let mut new_agent = DQNAgent::new(config)?;
new_agent.load_checkpoint(&checkpoint_path)?;
let metrics_after = new_agent.get_metrics();
// Verify metrics are restored
assert_eq!(metrics_before.total_episodes, metrics_after.total_episodes);
assert_eq!(metrics_before.total_steps, metrics_after.total_steps);
// Cleanup
std::fs::remove_file(checkpoint_path).ok();
Ok(())
}
#[tokio::test]
async fn test_dqn_reward_statistics_tracking() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig::default();
let mut agent = DQNAgent::new(config)?;
// Update reward statistics with various scenarios
agent.update_reward_stats(100.0, true);
agent.update_reward_stats(-50.0, false);
agent.update_reward_stats(0.0, false);
agent.update_reward_stats(200.0, true);
let stats = agent.get_training_stats();
assert_eq!(stats["total_episodes"], 4.0);
assert!(stats["avg_reward"] != 0.0);
assert!(stats["win_rate"] > 0.0);
assert!(stats["win_rate"] < 1.0);
Ok(())
}
// ============================================================================
// PPO Training Edge Cases
// ============================================================================
#[tokio::test]
async fn test_ppo_training_with_empty_trajectory_batch() -> Result<(), Box<dyn std::error::Error>> {
let config = PPOConfig::default();
let mut ppo = PPO::new(config)?;
let mut empty_batch = TrajectoryBatch::from_trajectories(vec![], vec![], vec![]);
// Training with empty batch should handle gracefully
let result = ppo.update(&mut empty_batch);
// Empty batch should either succeed with zero updates or fail gracefully
match result {
Ok((policy_loss, value_loss)) => {
assert!(policy_loss.is_finite());
assert!(value_loss.is_finite());
},
Err(_) => {
// Empty batch error is acceptable
},
}
Ok(())
}
#[tokio::test]
async fn test_ppo_training_with_single_step_trajectory() -> Result<(), Box<dyn std::error::Error>> {
let config = PPOConfig {
state_dim: 2,
mini_batch_size: 1,
batch_size: 1,
..Default::default()
};
let _ppo = PPO::new(config)?;
// Create trajectory with single step
let mut trajectory = Trajectory::new();
trajectory.add_step(TrajectoryStep::new(
vec![1.0, 2.0],
FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal),
-0.5,
10.0,
1.0,
true,
));
assert_eq!(trajectory.steps.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_ppo_training_with_all_terminal_states() -> Result<(), Box<dyn std::error::Error>> {
let config = PPOConfig {
state_dim: 2,
..Default::default()
};
let _ppo = PPO::new(config)?;
// Create trajectory with all terminal states
let mut trajectory = Trajectory::new();
for i in 0..10 {
trajectory.add_step(TrajectoryStep::new(
vec![i as f32, i as f32 + 1.0],
FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal),
-0.5,
5.0,
0.0,
true, // All steps are terminal
));
}
// Compute returns with all terminal states
let returns = trajectory.compute_returns(0.99);
assert_eq!(returns.len(), 10);
// All returns should be zero since all states are terminal
for ret in returns {
assert_eq!(ret, 0.0);
}
Ok(())
}
#[tokio::test]
async fn test_ppo_clip_epsilon_boundary() -> Result<(), Box<dyn std::error::Error>> {
// Test with very small epsilon (should still clip)
let config_small = PPOConfig {
clip_epsilon: 0.001,
..Default::default()
};
let _ppo_small = PPO::new(config_small)?;
// Test with large epsilon (less aggressive clipping)
let config_large = PPOConfig {
clip_epsilon: 0.9,
..Default::default()
};
let _ppo_large = PPO::new(config_large)?;
Ok(())
}
#[tokio::test]
async fn test_ppo_zero_entropy_coefficient() -> Result<(), Box<dyn std::error::Error>> {
let config = PPOConfig {
entropy_coeff: 0.0,
..Default::default()
};
let _ppo = PPO::new(config)?;
// PPO with zero entropy should work (deterministic policy)
Ok(())
}
#[tokio::test]
async fn test_ppo_high_entropy_coefficient() -> Result<(), Box<dyn std::error::Error>> {
let config = PPOConfig {
entropy_coeff: 1.0, // Very high entropy = more exploration
..Default::default()
};
let _ppo = PPO::new(config)?;
Ok(())
}
#[tokio::test]
async fn test_ppo_training_epochs_boundary() -> Result<(), Box<dyn std::error::Error>> {
// Test with 1 epoch
let config_single = PPOConfig {
num_epochs: 1,
..Default::default()
};
let _ppo_single = PPO::new(config_single)?;
// Test with many epochs
let config_many = PPOConfig {
num_epochs: 100,
..Default::default()
};
let _ppo_many = PPO::new(config_many)?;
Ok(())
}
// ============================================================================
// Liquid Neural Network Training Edge Cases
// ============================================================================
#[tokio::test]
async fn test_liquid_training_with_nan_loss() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
batch_size: 2,
max_epochs: 5,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
// Create batch with extreme values that could cause NaN
let inputs = vec![
vec![FixedPoint(i64::MAX / 2), FixedPoint(i64::MAX / 2)],
vec![FixedPoint(i64::MIN / 2), FixedPoint(i64::MIN / 2)],
];
let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
// Training should handle extreme values gracefully
let result = trainer.train(&mut network, &training_data, None);
// Either succeeds or fails with TrainingError (not panic)
match result {
Ok(_) => {},
Err(ml::liquid::LiquidError::TrainingError(_)) => {},
Err(e) => panic!("Unexpected error type: {:?}", e),
}
Ok(())
}
#[tokio::test]
async fn test_liquid_training_with_zero_learning_rate() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
learning_rate: FixedPoint(0), // Zero learning rate
batch_size: 2,
max_epochs: 3,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
let inputs = vec![
vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)],
vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)],
];
let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
// Training with zero learning rate should succeed but not learn
trainer.train(&mut network, &training_data, None)?;
// Learning rate should remain zero
assert_eq!(trainer.get_current_learning_rate(), 0.0);
Ok(())
}
#[tokio::test]
async fn test_liquid_training_early_stopping() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
batch_size: 2,
max_epochs: 100,
early_stopping_patience: 3, // Stop if no improvement for 3 epochs
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
// Training data
let train_inputs = vec![
vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)],
vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)],
];
let train_targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let train_batch = TrainingBatch::from_arrays(&train_inputs, &train_targets)?;
// Validation data
let val_inputs = vec![vec![FixedPoint(PRECISION / 6), FixedPoint(PRECISION / 7)]];
let val_targets = vec![vec![FixedPoint(PRECISION / 3)]];
let val_batch = TrainingBatch::from_arrays(&val_inputs, &val_targets)?;
let training_data = vec![train_batch];
let validation_data = vec![val_batch];
trainer.train(&mut network, &training_data, Some(&validation_data))?;
// Training should have stopped early (less than max epochs)
let history = trainer.get_training_history();
assert!(
history.len() < 100,
"Early stopping should trigger before max epochs"
);
Ok(())
}
#[tokio::test]
async fn test_liquid_training_batch_size_one() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
batch_size: 1, // Single sample per batch
max_epochs: 5,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
let inputs = vec![vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)]];
let targets = vec![vec![FixedPoint(PRECISION)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
// Training with batch size 1 should succeed
trainer.train(&mut network, &training_data, None)?;
Ok(())
}
#[tokio::test]
async fn test_liquid_training_gradient_clipping() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
gradient_clip_threshold: FixedPoint(PRECISION / 10), // Small threshold = aggressive clipping
batch_size: 2,
max_epochs: 3,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
let inputs = vec![
vec![FixedPoint(PRECISION), FixedPoint(PRECISION)],
vec![FixedPoint(-PRECISION), FixedPoint(-PRECISION)],
];
let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
// Training should apply gradient clipping
trainer.train(&mut network, &training_data, None)?;
Ok(())
}
#[tokio::test]
async fn test_liquid_training_adaptive_learning_rate() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
learning_rate: FixedPoint(PRECISION / 100), // 0.01
adaptive_learning_rate: true,
batch_size: 2,
max_epochs: 50,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
let initial_lr = trainer.get_current_learning_rate();
let inputs = vec![
vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)],
vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)],
];
let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
trainer.train(&mut network, &training_data, None)?;
let final_lr = trainer.get_current_learning_rate();
// Learning rate should have decayed
assert!(final_lr < initial_lr, "Adaptive learning rate should decay");
Ok(())
}
#[tokio::test]
async fn test_liquid_training_market_regime_adaptation() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
market_regime_adaptation: true,
batch_size: 2,
max_epochs: 5,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
// Create samples with different market regimes
let sample1 = TrainingSample {
input: vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)],
target: vec![FixedPoint(PRECISION)],
timestamp: Some(1000),
market_regime: Some(MarketRegime::Trending),
volatility: Some(FixedPoint(PRECISION * 2)), // High volatility
};
let sample2 = TrainingSample {
input: vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)],
target: vec![FixedPoint(PRECISION / 2)],
timestamp: Some(2000),
market_regime: Some(MarketRegime::Sideways),
volatility: Some(FixedPoint(PRECISION / 10)), // Low volatility
};
let batch = TrainingBatch::new(vec![sample1, sample2]);
let training_data = vec![batch];
// Training should adapt to market regimes
trainer.train(&mut network, &training_data, None)?;
Ok(())
}
#[tokio::test]
async fn test_liquid_training_l2_regularization() -> Result<(), Box<dyn std::error::Error>> {
let network_config = LiquidNetworkConfig {
network_type: NetworkType::LTC,
input_size: 2,
output_size: 1,
layer_configs: vec![],
output_layer: OutputLayerConfig {
use_linear_output: true,
output_activation: Some(ActivationType::Linear),
dropout_rate: None,
},
default_dt: FixedPoint::from_f64(0.01),
market_regime_adaptation: false,
};
let mut network = LiquidNetwork::new(network_config)?;
let training_config = LiquidTrainingConfig {
l2_regularization: FixedPoint(PRECISION / 100), // 0.01 L2 penalty
batch_size: 2,
max_epochs: 5,
..Default::default()
};
let mut trainer = LiquidTrainer::new(training_config);
let inputs = vec![
vec![FixedPoint(PRECISION / 2), FixedPoint(PRECISION / 4)],
vec![FixedPoint(PRECISION / 3), FixedPoint(PRECISION / 5)],
];
let targets = vec![vec![FixedPoint(PRECISION)], vec![FixedPoint(PRECISION / 2)]];
let batch = TrainingBatch::from_arrays(&inputs, &targets)?;
let training_data = vec![batch];
// Training with L2 regularization should succeed
trainer.train(&mut network, &training_data, None)?;
Ok(())
}
// ============================================================================
// Cross-Model Training Edge Cases
// ============================================================================
#[tokio::test]
async fn test_training_convergence_detection() -> Result<(), Box<dyn std::error::Error>> {
// Test convergence detection for DQN
let config = DQNConfig {
learning_rate: 0.0001,
..Default::default()
};
let mut agent = DQNAgent::new(config)?;
// Add identical experiences (should converge quickly)
for _ in 0..500 {
let experience = Experience::new(
vec![1.0; 52],
TradingAction::Hold.to_int(),
0.0,
vec![1.0; 52],
false,
);
agent.store_experience(experience)?;
}
let mut losses = Vec::new();
for _ in 0..10 {
let loss = agent.train()?;
losses.push(loss);
}
// Loss should decrease or stabilize (convergence)
let first_loss = losses[0];
let last_loss = losses[losses.len() - 1];
assert!(
last_loss <= first_loss * 1.5,
"Loss should not increase significantly"
);
Ok(())
}
#[tokio::test]
async fn test_training_with_mixed_terminal_non_terminal() -> Result<(), Box<dyn std::error::Error>>
{
let config = DQNConfig::default();
let mut agent = DQNAgent::new(config)?;
// Mix terminal and non-terminal experiences
for i in 0..400 {
let is_terminal = i % 10 == 0;
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::from_int((i % 3) as u8).unwrap().to_int(),
if is_terminal { 0.0 } else { 1.0 },
vec![(i + 1) as f32; 52],
is_terminal,
);
agent.store_experience(experience)?;
}
// Training should handle mixed terminal states
let loss = agent.train()?;
assert!(loss.is_finite());
Ok(())
}
#[tokio::test]
async fn test_training_metrics_accumulation() -> Result<(), Box<dyn std::error::Error>> {
let config = DQNConfig::default();
let mut agent = DQNAgent::new(config)?;
// Track metrics over multiple training steps
for i in 0..400 {
let experience = Experience::new(
vec![i as f32; 52],
TradingAction::Buy.to_int(),
1.0,
vec![i as f32 + 0.1; 52],
false,
);
agent.store_experience(experience)?;
}
agent.train()?;
agent.train()?;
agent.train()?;
let stats = agent.get_training_stats();
assert_eq!(stats["training_step"], 3.0);
assert!(stats["total_steps"] >= 3.0);
Ok(())
}