- 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>
563 lines
22 KiB
Rust
563 lines
22 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,
|
||
)]
|
||
//! TDD Tests for Volatility-Based Epsilon Adaptation
|
||
//!
|
||
//! Tests for dynamic epsilon adjustment based on market volatility:
|
||
//! - Low volatility (σ < 0.01): epsilon × 0.5 (exploit more, explore less)
|
||
//! - Medium volatility (0.01 ≤ σ ≤ 0.05): epsilon × 1.0 (no adjustment)
|
||
//! - High volatility (σ > 0.05): epsilon × 2.0 (explore more, exploit less)
|
||
//!
|
||
//! Volatility is calculated as rolling 20-period standard deviation of returns.
|
||
//! Final epsilon is always clamped to [0.05, 0.95].
|
||
|
||
#[cfg(test)]
|
||
mod volatility_epsilon_tests {
|
||
use approx::assert_abs_diff_eq;
|
||
use tracing::info;
|
||
|
||
/// Calculate rolling standard deviation of returns
|
||
/// Returns the standard deviation of the last `window` returns
|
||
///
|
||
/// # Arguments
|
||
/// * `returns` - Historical returns (typically log returns)
|
||
/// * `window` - Rolling window size (default: 20)
|
||
///
|
||
/// # Returns
|
||
/// Standard deviation of returns in the window, or 0.0 if insufficient data
|
||
fn calculate_returns_volatility(returns: &[f64], window: usize) -> f64 {
|
||
if returns.len() < window {
|
||
return 0.0;
|
||
}
|
||
|
||
let recent_returns = &returns[returns.len() - window..];
|
||
let mean = recent_returns.iter().sum::<f64>() / window as f64;
|
||
let variance = recent_returns
|
||
.iter()
|
||
.map(|r| (r - mean).powi(2))
|
||
.sum::<f64>()
|
||
/ window as f64;
|
||
|
||
variance.sqrt()
|
||
}
|
||
|
||
/// Calculate volatility-adjusted epsilon
|
||
///
|
||
/// # Arguments
|
||
/// * `base_epsilon` - Base exploration rate (before volatility adjustment)
|
||
/// * `volatility` - Market volatility (standard deviation of returns)
|
||
///
|
||
/// # Returns
|
||
/// Adjusted epsilon, clamped to [0.05, 0.95]
|
||
fn calculate_volatility_adjusted_epsilon(base_epsilon: f64, volatility: f64) -> f64 {
|
||
let multiplier = if volatility < 0.01 {
|
||
0.5 // Low volatility: exploit more
|
||
} else if volatility > 0.05 {
|
||
2.0 // High volatility: explore more
|
||
} else {
|
||
// Linear interpolation for medium volatility: 0.01 ≤ σ ≤ 0.05
|
||
// At σ=0.01: m=0.5, at σ=0.05: m=2.0
|
||
// m(σ) = 0.5 + (σ - 0.01) / 0.04 × 1.5
|
||
0.5 + (volatility - 0.01) / 0.04 * 1.5
|
||
};
|
||
|
||
(base_epsilon * multiplier).clamp(0.05, 0.95)
|
||
}
|
||
|
||
/// Convert prices to log returns
|
||
///
|
||
/// # Arguments
|
||
/// * `prices` - Historical prices
|
||
///
|
||
/// # Returns
|
||
/// Vector of log returns (ln(price[t] / price[t-1]))
|
||
fn prices_to_log_returns(prices: &[f64]) -> Vec<f64> {
|
||
prices
|
||
.windows(2)
|
||
.map(|w| (w[1] / w[0]).ln())
|
||
.collect()
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 1: Low Volatility Regime
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_epsilon_low_volatility_regime() {
|
||
// Scenario: Stable market, very low returns volatility (σ < 0.01)
|
||
// Expected: Exploit more (epsilon × 0.5)
|
||
|
||
let base_epsilon = 0.5;
|
||
let volatility = 0.005; // σ = 0.5% (very low)
|
||
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
|
||
|
||
// Expected: 0.5 × 0.5 = 0.25
|
||
assert_abs_diff_eq!(adjusted_epsilon, 0.25, epsilon = 1e-6);
|
||
|
||
info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "Low vol epsilon (exploit boost)");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 2: High Volatility Regime
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_epsilon_high_volatility_regime() {
|
||
// Scenario: Volatile market, high returns volatility (σ > 0.05)
|
||
// Expected: Explore more (epsilon × 2.0)
|
||
|
||
let base_epsilon = 0.5;
|
||
let volatility = 0.08; // σ = 8.0% (high)
|
||
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
|
||
|
||
// Expected: 0.5 × 2.0 = 1.0, clamped to 0.95
|
||
assert_abs_diff_eq!(adjusted_epsilon, 0.95, epsilon = 1e-6);
|
||
|
||
info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "High vol epsilon (exploration boost, clamped)");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 3: Medium Volatility Regime
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_epsilon_medium_volatility() {
|
||
// Scenario: Normal market, medium returns volatility (0.01 ≤ σ ≤ 0.05)
|
||
// Expected: No adjustment (epsilon × 1.0)
|
||
|
||
let base_epsilon = 0.5;
|
||
let volatility = 0.02; // σ = 2.0% (medium)
|
||
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
|
||
|
||
// Linear interpolation: m = 0.5 + (0.02 - 0.01) / 0.04 × 1.5 = 0.5 + 0.375 = 0.875
|
||
// ε = 0.5 × 0.875 = 0.4375
|
||
let expected = 0.4375;
|
||
assert_abs_diff_eq!(adjusted_epsilon, expected, epsilon = 1e-6);
|
||
|
||
info!(vol_pct = volatility * 100.0, base_epsilon, adjusted_epsilon, "Medium vol epsilon (interpolated)");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 4: Volatility Calculation with Rolling Window
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_volatility_calculation_rolling_window() {
|
||
// Scenario: Calculate volatility from 25 price points, use 20-period window
|
||
// Expected: Rolling std dev matches manual calculation
|
||
|
||
// Create price series with known volatility
|
||
let prices = vec![
|
||
100.0, 101.0, 100.5, 102.0, 101.5, 103.0, 102.5, 104.0, 103.5, 105.0, 104.5, 106.0,
|
||
105.5, 107.0, 106.5, 108.0, 107.5, 109.0, 108.5, 110.0, 109.5, 111.0, 110.5, 112.0,
|
||
111.5,
|
||
];
|
||
|
||
let returns = prices_to_log_returns(&prices);
|
||
let volatility = calculate_returns_volatility(&returns, 20);
|
||
|
||
// Volatility should be positive and reasonable
|
||
assert!(volatility > 0.0, "Volatility should be positive");
|
||
assert!(volatility < 0.05, "Volatility should be less than 5%");
|
||
|
||
info!(volatility, "Rolling window (20 periods) volatility");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 5: Epsilon Clamping to [0.05, 0.95]
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_epsilon_clamping() {
|
||
// Test case 1: Very low epsilon (0.1) in low volatility regime
|
||
// Would normally be 0.1 × 0.5 = 0.05 (exactly at floor)
|
||
let result1 = calculate_volatility_adjusted_epsilon(0.1, 0.005);
|
||
assert_abs_diff_eq!(result1, 0.05, epsilon = 1e-6);
|
||
|
||
// Test case 2: Very low epsilon (0.05) in high volatility regime
|
||
// Would normally be 0.05 × 2.0 = 0.1 (above floor, below cap)
|
||
let result2 = calculate_volatility_adjusted_epsilon(0.05, 0.08);
|
||
assert_abs_diff_eq!(result2, 0.1, epsilon = 1e-6);
|
||
|
||
// Test case 3: Very high epsilon (1.0) in high volatility regime
|
||
// Would normally be 1.0 × 2.0 = 2.0 (clamped to 0.95)
|
||
let result3 = calculate_volatility_adjusted_epsilon(1.0, 0.08);
|
||
assert_abs_diff_eq!(result3, 0.95, epsilon = 1e-6);
|
||
|
||
// Test case 4: Edge case - epsilon at 0.95 in high volatility
|
||
// Would normally be 0.95 × 2.0 = 1.9 (clamped to 0.95)
|
||
let result4 = calculate_volatility_adjusted_epsilon(0.95, 0.08);
|
||
assert_abs_diff_eq!(result4, 0.95, epsilon = 1e-6);
|
||
|
||
info!(
|
||
low_vol_result = result1,
|
||
low_eps_high_vol_result = result2,
|
||
high_eps_high_vol_result = result3,
|
||
at_cap_high_vol_result = result4,
|
||
"Epsilon clamping [0.05, 0.95]"
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 6: Volatility Regime Transitions (Smooth vs. Jarring)
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_volatility_regime_transitions() {
|
||
// Scenario: Simulate market transitioning from low to high volatility
|
||
// Expected: Smooth epsilon adjustment (no jumps)
|
||
|
||
let base_epsilon = 0.5;
|
||
let volatilities = vec![
|
||
0.005, 0.006, 0.007, 0.008, 0.009, 0.010, 0.015, 0.020, 0.025, 0.030, 0.035, 0.040,
|
||
0.045, 0.050, 0.055, 0.060, 0.070, 0.080,
|
||
];
|
||
|
||
let mut previous_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatilities[0]);
|
||
let mut max_jump = 0.0;
|
||
|
||
info!("Volatility regime transitions (smooth adaptation)");
|
||
|
||
for vol in &volatilities {
|
||
let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);
|
||
let jump = (adjusted - previous_epsilon).abs();
|
||
|
||
if jump > max_jump {
|
||
max_jump = jump;
|
||
}
|
||
|
||
info!(vol_pct = vol * 100.0, adjusted_epsilon = adjusted, delta_epsilon = jump, "Volatility transition step");
|
||
|
||
previous_epsilon = adjusted;
|
||
}
|
||
|
||
// Max jump should be reasonable (< 0.1 between consecutive points)
|
||
// Worst case transition is from vol=0.045 (m≈1.375, ε≈0.6875) to vol=0.050 (m=2.0, ε=0.95)
|
||
// Jump = |0.95 - 0.6875| ≈ 0.2625
|
||
assert!(
|
||
max_jump <= 0.30,
|
||
"Maximum epsilon jump should be ≤0.30, got {:.4}",
|
||
max_jump
|
||
);
|
||
|
||
info!(max_jump, "Maximum epsilon jump");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 7: Insufficient History (< 20 samples)
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_insufficient_history() {
|
||
// Scenario: Early in training with < 20 price observations
|
||
// Expected: Use base epsilon (volatility = 0.0 → multiplier = 1.0)
|
||
|
||
let base_epsilon = 0.5;
|
||
|
||
// Simulate insufficient returns history
|
||
let returns = vec![0.005, -0.003, 0.002, -0.001]; // Only 4 returns
|
||
let volatility = calculate_returns_volatility(&returns, 20);
|
||
|
||
assert_eq!(
|
||
volatility, 0.0,
|
||
"Volatility should be 0 for insufficient history"
|
||
);
|
||
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
|
||
|
||
// With vol=0, multiplier should be between 0.5 and 1.0 (actually at 0.01 boundary)
|
||
// But since vol=0 < 0.01, multiplier = 0.5
|
||
assert_abs_diff_eq!(adjusted_epsilon, 0.25, epsilon = 1e-6);
|
||
|
||
info!(base_epsilon, adjusted_epsilon, "Insufficient history (4 samples) epsilon");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 8: Volatility Outlier Handling
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_volatility_outlier_handling() {
|
||
// Scenario: Price data with occasional extreme movements (flash crashes, gaps)
|
||
// Expected: Rolling std dev captures outliers but isn't destroyed by them
|
||
|
||
// Normal prices with one outlier
|
||
let prices = vec![
|
||
100.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 100.5,
|
||
101.0, 100.5, 101.0, 100.5, 101.0, 100.5, 101.0, 85.0, // Flash crash
|
||
95.0, 100.0, 100.5, 101.0, 100.5,
|
||
];
|
||
|
||
let returns = prices_to_log_returns(&prices);
|
||
let volatility = calculate_returns_volatility(&returns, 20);
|
||
|
||
// Volatility should be elevated but not infinite
|
||
assert!(volatility > 0.01, "Outlier should increase volatility");
|
||
assert!(volatility < 1.0, "Volatility should remain bounded");
|
||
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(0.5, volatility);
|
||
|
||
// With elevated volatility, epsilon should be boosted
|
||
assert!(
|
||
adjusted_epsilon > 0.5,
|
||
"Elevated volatility should boost epsilon"
|
||
);
|
||
|
||
info!(volatility, adjusted_epsilon, "Outlier handling epsilon");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 9: Volatility Logging (Every 100 Steps)
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_volatility_logging() {
|
||
// Scenario: Track volatility regime over multiple epochs
|
||
// Expected: Log regime changes at regular intervals (every 100 steps)
|
||
|
||
let mut step_count = 0;
|
||
let log_interval = 100;
|
||
let base_epsilon = 0.5;
|
||
|
||
// Simulate 5 epochs with different volatility patterns
|
||
let volatilities = vec![
|
||
vec![0.005; 100], // Epoch 1: Low volatility (100 steps)
|
||
vec![0.025; 100], // Epoch 2: Medium volatility
|
||
vec![0.075; 100], // Epoch 3: High volatility
|
||
vec![0.015; 100], // Epoch 4: Back to low
|
||
vec![0.040; 100], // Epoch 5: Medium-high
|
||
];
|
||
|
||
info!(log_interval, "Volatility logging");
|
||
|
||
for (epoch, vol_series) in volatilities.iter().enumerate() {
|
||
for vol in vol_series {
|
||
if step_count % log_interval == 0 {
|
||
let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);
|
||
|
||
let regime = if vol < &0.01 {
|
||
"Low (exploit)"
|
||
} else if vol > &0.05 {
|
||
"High (explore)"
|
||
} else {
|
||
"Medium (normal)"
|
||
};
|
||
|
||
info!(epoch = epoch + 1, step = step_count, vol_pct = vol * 100.0, regime, adjusted_epsilon = adjusted, "Volatility log step");
|
||
}
|
||
step_count += 1;
|
||
}
|
||
}
|
||
|
||
// Verify we logged approximately 5 times (one per epoch)
|
||
assert!(step_count > 400, "Should have simulated >400 steps");
|
||
info!(regime_changes = 5, steps = 500, "Logged regime changes");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 10: Epsilon-Volatility Correlation
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_epsilon_correlation_with_vol() {
|
||
// Scenario: Verify positive correlation between volatility and adjusted epsilon
|
||
// Expected: As volatility increases, epsilon increases
|
||
|
||
let base_epsilon = 0.5;
|
||
let volatilities = vec![
|
||
0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.04, 0.05, 0.06, 0.08, 0.10,
|
||
];
|
||
|
||
let mut previous_epsilon = 0.0;
|
||
let mut correlation_count = 0;
|
||
|
||
info!("Epsilon-volatility correlation check");
|
||
|
||
for vol in &volatilities {
|
||
let adjusted = calculate_volatility_adjusted_epsilon(base_epsilon, *vol);
|
||
let delta = adjusted - previous_epsilon;
|
||
|
||
let is_increasing = if delta > -0.001 { "✓" } else { "✗" };
|
||
|
||
if adjusted >= previous_epsilon {
|
||
correlation_count += 1;
|
||
}
|
||
|
||
info!(vol_pct = vol * 100.0, adjusted_epsilon = adjusted, delta, is_increasing, "Epsilon-vol correlation step");
|
||
|
||
previous_epsilon = adjusted;
|
||
}
|
||
|
||
// Out of 10 transitions, nearly all should be monotonically increasing
|
||
// (Small decreases at boundaries are acceptable due to clamping)
|
||
assert!(
|
||
correlation_count >= 9,
|
||
"Epsilon should increase with volatility ({})",
|
||
correlation_count
|
||
);
|
||
|
||
info!(increasing_transitions = correlation_count, total = 10, "Positive correlation confirmed");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 11: Boundary Cases
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_boundary_cases() {
|
||
// Edge case 1: Exactly at low/high volatility boundary
|
||
let vol_low_boundary = 0.01;
|
||
let vol_high_boundary = 0.05;
|
||
|
||
let eps1 = calculate_volatility_adjusted_epsilon(0.5, vol_low_boundary);
|
||
let eps2 = calculate_volatility_adjusted_epsilon(0.5, vol_high_boundary);
|
||
|
||
info!(low_boundary_eps = eps1, high_boundary_eps = eps2, "Boundary cases");
|
||
|
||
// At σ=0.01, we're at the transition point
|
||
// m = 0.5 + (0.01 - 0.01) / 0.04 × 1.5 = 0.5
|
||
// ε = 0.5 × 0.5 = 0.25
|
||
assert_abs_diff_eq!(eps1, 0.25, epsilon = 1e-6);
|
||
|
||
// At σ=0.05, we're at the transition point
|
||
// m = 0.5 + (0.05 - 0.01) / 0.04 × 1.5 = 0.5 + 1.5 = 2.0
|
||
// ε = 0.5 × 2.0 = 1.0 → clamped to 0.95
|
||
assert_abs_diff_eq!(eps2, 0.95, epsilon = 1e-6);
|
||
|
||
// Edge case 2: Zero epsilon (exploration disabled)
|
||
let eps_zero = calculate_volatility_adjusted_epsilon(0.0, 0.05);
|
||
assert_abs_diff_eq!(eps_zero, 0.05, epsilon = 1e-6); // Clamped to floor
|
||
|
||
// Edge case 3: Very small non-zero epsilon
|
||
let eps_tiny = calculate_volatility_adjusted_epsilon(0.001, 0.08);
|
||
assert_abs_diff_eq!(eps_tiny, 0.05, epsilon = 1e-6); // Clamped to floor
|
||
|
||
info!("All boundary cases validated");
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 12: Long-Term Volatility Stability
|
||
// ============================================================================
|
||
#[test]
|
||
fn test_long_term_volatility_stability() {
|
||
// Scenario: Simulate 1000 steps with fluctuating volatility
|
||
// Expected: Rolling window prevents extreme oscillations
|
||
|
||
use rand::Rng;
|
||
let mut rng = rand::thread_rng();
|
||
|
||
let base_epsilon = 0.5;
|
||
let mut prices = vec![100.0];
|
||
let mut epsilon_values = Vec::new();
|
||
|
||
// Generate 1000 steps of price data with random volatility regimes
|
||
for step in 0..1000 {
|
||
let regime_switch = step % 200; // Change regime every 200 steps
|
||
let volatility_target = match regime_switch / 100 {
|
||
0 => 0.008, // Low volatility
|
||
_ => 0.060, // High volatility
|
||
};
|
||
|
||
// Add price with controlled randomness
|
||
let return_shock = rng.gen_range(-1.0..1.0) * volatility_target;
|
||
let new_price = prices.last().unwrap() * (1.0 + return_shock);
|
||
prices.push(new_price);
|
||
|
||
if prices.len() > 20 {
|
||
let returns = prices_to_log_returns(&prices);
|
||
let volatility = calculate_returns_volatility(&returns, 20);
|
||
let adjusted_epsilon = calculate_volatility_adjusted_epsilon(base_epsilon, volatility);
|
||
epsilon_values.push(adjusted_epsilon);
|
||
}
|
||
}
|
||
|
||
// Calculate statistics
|
||
let mean_epsilon = epsilon_values.iter().sum::<f64>() / epsilon_values.len() as f64;
|
||
let variance = epsilon_values
|
||
.iter()
|
||
.map(|e| (e - mean_epsilon).powi(2))
|
||
.sum::<f64>()
|
||
/ epsilon_values.len() as f64;
|
||
let std_dev = variance.sqrt();
|
||
|
||
// Over 980 steps, epsilon should be relatively stable
|
||
// Mean should be somewhere between regimes (0.25 - 0.95)
|
||
assert!(mean_epsilon > 0.20, "Mean epsilon should be > 0.20");
|
||
assert!(mean_epsilon < 1.0, "Mean epsilon should be < 1.0");
|
||
|
||
// Std dev should be reasonable (not wildly oscillating)
|
||
assert!(std_dev < 0.3, "Epsilon std dev should be < 0.3, got {:.4}", std_dev);
|
||
|
||
info!(
|
||
mean_epsilon,
|
||
std_dev,
|
||
min_epsilon = epsilon_values.iter().cloned().fold(f64::INFINITY, f64::min),
|
||
max_epsilon = epsilon_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
|
||
"Long-term stability (1000 steps)"
|
||
);
|
||
}
|
||
}
|