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>
246 lines
7.9 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|