Files
foxhunt/ml/tests/ppo_stress_testing_tests.rs
jgrusewski c645e6222d Wave 11: Rainbow DQN integration + 23/23 tests passing
CRITICAL FINDINGS from 3-trial validation:
- 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION
- Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false
- Negative Q-values confirmed: HOLD -1000 to -3250
- Performance: Sharpe 0.29 (target 0.77)

Changes:
- Fixed N-Step compilation (7/7 tests passing)
- Fixed Distributional compilation (6/6 tests passing)
- Fixed Dueling CUDA errors (10/10 tests passing)
- Added TDD validation for state_dim=225
- Total: 23/23 Wave 11 tests passing (100%)

Issues requiring investigation:
1. Why are Dueling/Distributional/Noisy disabled in hyperopt?
2. Why gradient explosion despite previous fixes?
3. Test coverage gaps - unit tests pass but integration fails

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:53:59 +01:00

246 lines
7.9 KiB
Rust

//! PPO Stress Testing Framework Tests
//!
//! TDD Red Phase: Tests written BEFORE implementation.
//! These tests will initially fail and guide the implementation.
use ml::ppo::stress_testing::{
PPOStressTester, StressScenario,
flash_crash_scenario, liquidity_crisis_scenario,
};
use ml::hyperopt::adapters::PPOTrainer;
use anyhow::Result;
/// Helper to create a minimal PPO trainer for testing
fn create_test_trainer() -> Result<PPOTrainer> {
// PPOTrainer requires a DBN data directory and episodes count
// For stress testing, we use the test data directory
// Tests run from the workspace root, so we need to go up one level from ml/
let data_dir = "../test_data";
let episodes = 100;
PPOTrainer::new(data_dir, episodes)
}
#[test]
fn test_flash_crash_scenario() -> Result<()> {
// TDD Red Phase: This test will fail until implementation exists
// Setup
let trainer = create_test_trainer()?;
let mut stress_tester = PPOStressTester::new(trainer);
let scenario = flash_crash_scenario();
// Execute stress test
let result = stress_tester.run_scenario(&scenario)?;
// Validate flash crash scenario parameters
assert_eq!(result.scenario_name, "Flash Crash");
assert_eq!(scenario.price_shock_pct, -10.0, "Flash crash should be -10% shock");
assert_eq!(scenario.volatility_multiplier, 3.0, "Flash crash should be 3x volatility");
assert_eq!(scenario.spread_multiplier, 10.0, "Flash crash should be 10x spreads");
assert_eq!(scenario.duration_steps, 300, "Flash crash should last 300 steps (5 min)");
// Validate robustness criteria
assert!(
result.max_drawdown <= scenario.max_drawdown_threshold,
"Max drawdown {:.2}% exceeded threshold {:.2}%",
result.max_drawdown,
scenario.max_drawdown_threshold
);
assert!(
!result.bankruptcy,
"Agent went bankrupt during flash crash (portfolio <= 0)"
);
assert!(
result.action_diversity >= scenario.min_action_diversity,
"Action diversity {:.2}% below threshold {:.2}%",
result.action_diversity,
scenario.min_action_diversity
);
println!("✅ Flash Crash Test: Drawdown={:.2}%, Diversity={:.2}%",
result.max_drawdown, result.action_diversity);
Ok(())
}
#[test]
fn test_liquidity_crisis_scenario() -> Result<()> {
// TDD Red Phase: This test will fail until implementation exists
// Setup
let trainer = create_test_trainer()?;
let mut stress_tester = PPOStressTester::new(trainer);
let scenario = liquidity_crisis_scenario();
// Execute stress test
let result = stress_tester.run_scenario(&scenario)?;
// Validate liquidity crisis scenario parameters
assert_eq!(result.scenario_name, "Liquidity Crisis");
assert_eq!(scenario.price_shock_pct, -2.0, "Liquidity crisis should be -2% shock");
assert_eq!(scenario.volatility_multiplier, 2.0, "Liquidity crisis should be 2x volatility");
assert_eq!(scenario.spread_multiplier, 50.0, "Liquidity crisis should be 50x spreads");
assert_eq!(scenario.duration_steps, 600, "Liquidity crisis should last 600 steps (10 min)");
// Validate agent survived extreme spread widening
assert!(
!result.bankruptcy,
"Agent went bankrupt during liquidity crisis"
);
assert!(
result.max_drawdown <= scenario.max_drawdown_threshold,
"Max drawdown {:.2}% exceeded threshold {:.2}%",
result.max_drawdown,
scenario.max_drawdown_threshold
);
println!("✅ Liquidity Crisis Test: Drawdown={:.2}%, Final Portfolio={:.2}%",
result.max_drawdown, result.final_portfolio_pct);
Ok(())
}
#[test]
fn test_bankruptcy_detection() -> Result<()> {
// TDD Red Phase: Test bankruptcy detection logic
// Setup
let trainer = create_test_trainer()?;
let mut stress_tester = PPOStressTester::new(trainer);
// Create extreme scenario designed to cause bankruptcy
let extreme_scenario = StressScenario {
name: "Extreme Bankruptcy Test".to_string(),
price_shock_pct: -95.0, // 95% crash (unrealistic but tests edge case)
volatility_multiplier: 10.0,
spread_multiplier: 100.0,
duration_steps: 100,
max_drawdown_threshold: 100.0, // Accept any drawdown for this test
min_action_diversity: 0.0, // No diversity requirement
};
// Execute stress test
let result = stress_tester.run_scenario(&extreme_scenario)?;
// Validate bankruptcy was detected
assert!(
result.bankruptcy,
"Bankruptcy should be detected when portfolio <= 0"
);
assert!(
!result.passed,
"Test should FAIL when bankruptcy occurs"
);
assert!(
result.failure_reasons.iter().any(|r| r.contains("Bankruptcy")),
"Failure reasons should mention bankruptcy: {:?}",
result.failure_reasons
);
println!("✅ Bankruptcy Detection Test: Correctly detected portfolio collapse");
Ok(())
}
#[test]
fn test_metrics_collection() -> Result<()> {
// TDD Red Phase: Test metrics collection logic
// Setup
let trainer = create_test_trainer()?;
let mut stress_tester = PPOStressTester::new(trainer);
let scenario = flash_crash_scenario();
// Execute stress test
let result = stress_tester.run_scenario(&scenario)?;
// Validate all metrics are collected
assert!(
result.max_drawdown >= 0.0,
"Max drawdown should be non-negative: {:.2}%",
result.max_drawdown
);
assert!(
result.action_diversity >= 0.0 && result.action_diversity <= 100.0,
"Action diversity should be 0-100%: {:.2}%",
result.action_diversity
);
assert!(
result.total_trades > 0,
"Should have executed trades during stress test"
);
assert!(
result.q_value_std >= 0.0,
"Q-value standard deviation should be non-negative: {:.4}",
result.q_value_std
);
assert!(
result.execution_time_ms > 0,
"Execution time should be positive: {} ms",
result.execution_time_ms
);
// Validate result has scenario name
assert_eq!(
result.scenario_name,
scenario.name,
"Result should contain scenario name"
);
println!("✅ Metrics Collection Test: All metrics tracked correctly");
println!(" - Max Drawdown: {:.2}%", result.max_drawdown);
println!(" - Portfolio Change: {:.2}%", result.final_portfolio_pct);
println!(" - Action Diversity: {:.2}%", result.action_diversity);
println!(" - Total Trades: {}", result.total_trades);
println!(" - Q-Value Std: {:.4}", result.q_value_std);
println!(" - Execution Time: {} ms", result.execution_time_ms);
Ok(())
}
#[test]
fn test_scenario_definitions() {
// Validate all 8 predefined scenarios exist and have reasonable parameters
let scenarios = vec![
flash_crash_scenario(),
liquidity_crisis_scenario(),
ml::ppo::stress_testing::vix_spike_scenario(),
ml::ppo::stress_testing::trending_market_scenario(),
ml::ppo::stress_testing::whipsaw_scenario(),
ml::ppo::stress_testing::gap_risk_scenario(),
ml::ppo::stress_testing::correlation_breakdown_scenario(),
ml::ppo::stress_testing::multi_asset_stress_scenario(),
];
assert_eq!(scenarios.len(), 8, "Should have 8 predefined scenarios");
// Verify all scenarios have reasonable thresholds
for scenario in scenarios {
assert!(
scenario.max_drawdown_threshold > 0.0,
"Max drawdown threshold must be positive: {}",
scenario.name
);
assert!(
scenario.min_action_diversity >= 0.0 && scenario.min_action_diversity <= 100.0,
"Action diversity must be 0-100%: {}",
scenario.name
);
assert!(
scenario.duration_steps > 0,
"Duration must be positive: {}",
scenario.name
);
}
}