- Reduce CI GPU test datasets 16x for walltime reduction - Reduce early-stop epochs 50→10, add --test-threads=1 - Serialize all GPU lib tests to prevent cuBLAS init race - Align state_dim to 16 for BF16 tensor core HMMA dispatch - BF16 precision tolerance in ml-dqn tests - Enable branching DQN + tracing subscriber in smoke tests - Prevent min_replay_size > buffer_size deadlock in early-stop tests - Prevent AutoReplaySizer from breaking gradient collapse warmup - Replace racy tokio::spawn checkpoint counter with AtomicUsize - Set warmup_steps=0 and max_training_steps_per_epoch=300 in early-stop tests - RealDataLoader respects TEST_DATA_DIR for CI PVC layout - Add collapse_warmup_capacity to gpu_smoketest DQNConfig - Drain CUDA context between test binaries - Detached HEAD checkout prevents local branch corruption - GPU pipeline tests: fix BF16 dtype and rank-1 squeeze assertions - OOD input handling tests use use_gpu: true Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
319 lines
10 KiB
Rust
319 lines
10 KiB
Rust
#![allow(
|
|
clippy::assertions_on_constants,
|
|
clippy::assertions_on_result_states,
|
|
clippy::clone_on_copy,
|
|
clippy::decimal_literal_representation,
|
|
clippy::doc_markdown,
|
|
clippy::empty_line_after_doc_comments,
|
|
clippy::field_reassign_with_default,
|
|
clippy::get_unwrap,
|
|
clippy::identity_op,
|
|
clippy::inconsistent_digit_grouping,
|
|
clippy::indexing_slicing,
|
|
clippy::integer_division,
|
|
clippy::len_zero,
|
|
clippy::let_underscore_must_use,
|
|
clippy::manual_div_ceil,
|
|
clippy::manual_let_else,
|
|
clippy::manual_range_contains,
|
|
clippy::modulo_arithmetic,
|
|
clippy::needless_range_loop,
|
|
clippy::non_ascii_literal,
|
|
clippy::redundant_clone,
|
|
clippy::shadow_reuse,
|
|
clippy::shadow_same,
|
|
clippy::shadow_unrelated,
|
|
clippy::single_match_else,
|
|
clippy::str_to_string,
|
|
clippy::string_slice,
|
|
clippy::tests_outside_test_module,
|
|
clippy::too_many_lines,
|
|
clippy::unnecessary_wraps,
|
|
clippy::unseparated_literal_suffix,
|
|
clippy::use_debug,
|
|
clippy::useless_vec,
|
|
clippy::wildcard_enum_match_arm,
|
|
clippy::else_if_without_else,
|
|
clippy::expect_used,
|
|
clippy::missing_const_for_fn,
|
|
clippy::similar_names,
|
|
clippy::type_complexity,
|
|
clippy::collapsible_else_if,
|
|
clippy::doc_lazy_continuation,
|
|
clippy::items_after_test_module,
|
|
clippy::map_clone,
|
|
clippy::multiple_unsafe_ops_per_block,
|
|
clippy::unwrap_or_default,
|
|
clippy::assign_op_pattern,
|
|
clippy::needless_borrow,
|
|
clippy::println_empty_string,
|
|
clippy::unnecessary_cast,
|
|
clippy::used_underscore_binding,
|
|
clippy::create_dir,
|
|
clippy::implicit_saturating_sub,
|
|
clippy::exit,
|
|
clippy::expect_fun_call,
|
|
clippy::too_many_arguments,
|
|
clippy::unnecessary_map_or,
|
|
clippy::unwrap_used,
|
|
dead_code,
|
|
unused_imports,
|
|
unused_variables,
|
|
clippy::cloned_ref_to_slice_refs,
|
|
clippy::neg_multiply,
|
|
clippy::while_let_loop,
|
|
clippy::bool_assert_comparison,
|
|
clippy::excessive_precision,
|
|
clippy::trivially_copy_pass_by_ref,
|
|
clippy::op_ref,
|
|
clippy::redundant_closure,
|
|
clippy::unnecessary_lazy_evaluations,
|
|
clippy::if_then_some_else_none,
|
|
clippy::unnecessary_to_owned,
|
|
clippy::single_component_path_imports,
|
|
)]
|
|
//! 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;
|
|
use tracing::{info, warn};
|
|
|
|
/// 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
|
|
);
|
|
|
|
info!(max_drawdown = %format!("{:.2}", result.max_drawdown), action_diversity = %format!("{:.2}", result.action_diversity), "Flash Crash Test passed");
|
|
|
|
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
|
|
);
|
|
|
|
info!(max_drawdown = %format!("{:.2}", result.max_drawdown), final_portfolio_pct = %format!("{:.2}", result.final_portfolio_pct), "Liquidity Crisis Test passed");
|
|
|
|
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
|
|
);
|
|
|
|
info!("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"
|
|
);
|
|
|
|
info!("Metrics Collection Test: All metrics tracked correctly");
|
|
info!(max_drawdown = %format!("{:.2}", result.max_drawdown), "Max Drawdown (%)");
|
|
info!(final_portfolio_pct = %format!("{:.2}", result.final_portfolio_pct), "Portfolio Change (%)");
|
|
info!(action_diversity = %format!("{:.2}", result.action_diversity), "Action Diversity (%)");
|
|
info!(total_trades = result.total_trades, "Total Trades");
|
|
info!(q_value_std = %format!("{:.4}", result.q_value_std), "Q-Value Std");
|
|
info!(execution_time_ms = result.execution_time_ms, "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
|
|
);
|
|
}
|
|
}
|