Files
foxhunt/ml/tests/pages_test_test.rs
jgrusewski cb515363a9 fix(warnings): Eliminate 136 warnings across workspace via 11 parallel agents
## Summary
Pre-commit warning regression fix wave - deployed 11 parallel Task agents to systematically eliminate all compilation errors (2) and warnings (136) across the entire workspace.

## Changes by Category

### P0 Compilation Fixes (2 errors → 0)
- ml/src/hyperopt/adapters/mamba2.rs: Added missing `trial_counter: 0` to test initializers (lines 1135, 1165)

### ML Crate Warnings (35 → 0)
- ml/src/hyperopt/tests.rs: Added `#[allow(deprecated)]` for test-specific deprecated function usage
- ml/src/ensemble/ab_testing.rs: Renamed unused variables (_control_count, _rng)
- ml/src/security/*.rs: Fixed unused loop variables (i → _)
- ml/src/tft/quantized_attention.rs: Renamed unused test variable (_v)
- ml/src/features/regime_adaptive.rs: Renamed unused variables (_adaptive)
- ml/src/regime/{orchestrator,ranging}.rs: Renamed unused variables

### Data Crate Fixes (28 warnings + 4 errors → 0)
- data/Cargo.toml: Moved clap from [dev-dependencies] to [dependencies] (examples require it)
- data/examples/validate_cl_fut.rs: Updated to databento 0.42.0 API (decode_record_ref loop pattern)
- data/examples/download_mbp10_data.rs: Fixed reqwest 0.12 API (bytes_stream → chunk)
- data/examples/*.rs: Removed unused imports (4 files via cargo fix)
- data/tests/real_data_helpers.rs: Added `#[allow(dead_code)]` to cross-binary test helpers

### API Gateway Test Warnings (19 → 0)
- services/api_gateway/tests/common/mod.rs: Added `#[allow(dead_code)]` to shared test utilities (6 items)
- services/api_gateway/tests/rate_limiting_tests.rs: Added `#[allow(dead_code)]` to REDIS_URL constant

## Verification
```bash
cargo check --workspace
# Result: Finished in 49.41s
# Warnings: 0 (was 136)
# Errors: 0 (was 2)
```

## Files Modified: 26 total
- ML: 14 files (9 manual + 5 auto-fixed)
- Data: 10 files (2 Cargo.toml + 6 examples + 1 test + 1 dependency update)
- API Gateway: 2 test files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:15:09 +01:00

507 lines
15 KiB
Rust

//! Comprehensive TDD Tests for PAGES Variance Changepoint Detection
//!
//! Test coverage:
//! 1. Basic functionality (initialization, stable variance)
//! 2. Variance change detection (increase, decrease)
//! 3. Edge cases (zero variance, rapid changes)
//! 4. Real market data integration (ES.FUT, NQ.FUT volatility regimes)
//! 5. Performance benchmarks (<80μs target)
use anyhow::Result;
use ml::regime::pages_test::PAGESTest;
use std::time::Instant;
// ============================================================================
// Unit Tests: Basic Functionality
// ============================================================================
#[test]
fn test_pages_default_initialization() {
let pages = PAGESTest::default();
assert_eq!(pages.get_target_variance(), 1.0);
assert_eq!(pages.get_drift_allowance(), 0.5);
assert_eq!(pages.get_detection_threshold(), 5.0);
}
#[test]
fn test_pages_custom_initialization() {
let pages = PAGESTest::new(2.0, 1.0, 8.0, 50);
assert_eq!(pages.get_target_variance(), 2.0);
assert_eq!(pages.get_drift_allowance(), 1.0);
assert_eq!(pages.get_detection_threshold(), 8.0);
}
#[test]
#[should_panic(expected = "Target variance must be positive")]
fn test_pages_negative_target_variance_panics() {
PAGESTest::new(-1.0, 0.5, 5.0, 20);
}
#[test]
#[should_panic(expected = "Window size must be at least 2")]
fn test_pages_invalid_window_size_panics() {
PAGESTest::new(1.0, 0.5, 5.0, 1);
}
// ============================================================================
// Unit Tests: Variance Computation
// ============================================================================
#[test]
fn test_pages_variance_computation_known_values() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Add values: [1, 2, 3, 4, 5]
// Mean = 3, Variance = 2.5 (sample variance with Bessel correction)
for val in [1.0, 2.0, 3.0, 4.0, 5.0] {
pages.update(val)?;
}
let variance = pages.get_current_variance();
let expected_variance = 2.5; // Sample variance of [1,2,3,4,5]
assert!(
(variance - expected_variance).abs() < 0.01,
"Expected variance ~{}, got {}",
expected_variance,
variance
);
Ok(())
}
#[test]
fn test_pages_rolling_window_behavior() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 5);
// Add 10 values, should keep only last 5
for i in 1..=10 {
pages.update(i as f64)?;
}
assert_eq!(pages.get_window_fill(), 5);
assert_eq!(pages.get_update_count(), 10);
// Variance should be computed on [6, 7, 8, 9, 10]
// Mean = 8, Variance = 2.5
let variance = pages.get_current_variance();
assert!((variance - 2.5).abs() < 0.01);
Ok(())
}
// ============================================================================
// Unit Tests: Stable Variance (No Detection)
// ============================================================================
#[test]
fn test_pages_stable_variance_no_false_alarms() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Generate 100 values from N(0, 1) distribution (variance = 1)
use rand_distr::{Distribution, Normal};
let normal = Normal::new(0.0, 1.0).unwrap();
let mut rng = rand::thread_rng();
for _ in 0..100 {
let value = normal.sample(&mut rng);
let result = pages.update(value)?;
assert!(
result.is_none(),
"Should not detect change when variance is stable at target"
);
}
// Cumulative sum should stay near zero with stable variance
assert!(
pages.get_cumulative_sum() < 2.0,
"Cumulative sum should be low for stable variance"
);
Ok(())
}
#[test]
fn test_pages_zero_variance_no_crash() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Feed constant value (zero variance)
for _ in 0..30 {
let result = pages.update(5.0)?;
assert!(
result.is_none(),
"Zero variance should not trigger detection"
);
}
assert_eq!(pages.get_current_variance(), 0.0);
assert_eq!(pages.get_cumulative_sum(), 0.0);
Ok(())
}
// ============================================================================
// Unit Tests: Variance Increase Detection
// ============================================================================
#[test]
fn test_pages_variance_increase_detection_synthetic() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.25, 4.0, 20);
// Phase 1: Stable variance ≈ 1.0 (30 samples)
use rand_distr::{Distribution, Normal};
let normal_stable = Normal::new(0.0, 1.0).unwrap();
let mut rng = rand::thread_rng();
for _ in 0..30 {
pages.update(normal_stable.sample(&mut rng))?;
}
// Phase 2: Increased variance ≈ 4.0 (2x std dev)
let normal_volatile = Normal::new(0.0, 2.0).unwrap();
let mut detected = false;
let mut detection_lag = 0;
for _ in 0..50 {
detection_lag += 1;
let value = normal_volatile.sample(&mut rng);
if let Some(change) = pages.update(value)? {
detected = true;
assert!(
change.variance_ratio > 2.0,
"Should detect significant variance increase (ratio > 2.0)"
);
assert_eq!(change.target_variance, 1.0);
assert!(change.pages_statistic > 4.0);
println!(
"Detected variance increase at lag {} samples, ratio: {:.2}",
detection_lag, change.variance_ratio
);
break;
}
}
assert!(
detected,
"Should detect variance increase within 50 samples"
);
assert!(
detection_lag < 30,
"Detection lag should be reasonable (<30 samples)"
);
Ok(())
}
#[test]
fn test_pages_large_variance_spike() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Stable phase
for i in 0..20 {
pages.update(if i % 2 == 0 { 1.0 } else { -1.0 })?;
}
// Sudden large spike (10x variance increase)
let mut detected = false;
for i in 0..20 {
let value = if i % 2 == 0 { 10.0 } else { -10.0 };
if let Some(change) = pages.update(value)? {
detected = true;
assert!(
change.variance_ratio > 5.0,
"Should detect large variance spike"
);
break;
}
}
assert!(detected, "Should quickly detect large variance spike");
Ok(())
}
// ============================================================================
// Unit Tests: Variance Decrease Detection
// ============================================================================
#[test]
fn test_pages_variance_decrease_detection() -> Result<()> {
// PAGES test with low target variance (monitoring for increases from low baseline)
// For decrease detection, we need high target variance
let mut pages = PAGESTest::new(4.0, 0.5, 5.0, 20);
// Phase 1: High variance ≈ 4.0
use rand_distr::{Distribution, Normal};
let normal_volatile = Normal::new(0.0, 2.0).unwrap();
let mut rng = rand::thread_rng();
for _ in 0..30 {
pages.update(normal_volatile.sample(&mut rng))?;
}
// Phase 2: Decreased variance ≈ 1.0
let normal_stable = Normal::new(0.0, 1.0).unwrap();
// For decrease detection with one-sided CUSUM, we need to invert the logic
// or use two-sided test. For now, verify that variance does decrease
// but may not trigger alarm (one-sided test monitors increases)
for _ in 0..30 {
pages.update(normal_stable.sample(&mut rng))?;
}
let current_var = pages.get_current_variance();
assert!(
current_var < 2.0,
"Variance should have decreased from 4.0 to ~1.0"
);
// Note: One-sided PAGES primarily detects increases relative to target
// For comprehensive variance monitoring, use two-sided test or separate
// PAGES instances for increase and decrease
Ok(())
}
// ============================================================================
// Unit Tests: Reset Functionality
// ============================================================================
#[test]
fn test_pages_reset_clears_state() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Accumulate state
for i in 0..25 {
pages.update((i as f64) * 2.0)?;
}
assert!(pages.get_window_fill() > 0);
assert!(pages.get_update_count() > 0);
assert!(pages.get_cumulative_sum() >= 0.0);
// Reset
pages.reset();
// Verify all state cleared
assert_eq!(pages.get_window_fill(), 0);
assert_eq!(pages.get_update_count(), 0);
assert_eq!(pages.get_cumulative_sum(), 0.0);
assert_eq!(pages.get_current_variance(), 0.0);
// Verify can start fresh analysis
pages.update(1.0)?;
assert_eq!(pages.get_update_count(), 1);
Ok(())
}
// ============================================================================
// Unit Tests: Error Handling
// ============================================================================
#[test]
fn test_pages_rejects_nan() {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
let result = pages.update(f64::NAN);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("non-finite"));
}
#[test]
fn test_pages_rejects_infinity() {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
let result = pages.update(f64::INFINITY);
assert!(result.is_err());
let result = pages.update(f64::NEG_INFINITY);
assert!(result.is_err());
}
// ============================================================================
// Integration Tests: Real Market Data (ES.FUT, NQ.FUT)
// ============================================================================
#[test]
#[ignore = "Requires DBN test data files"]
fn test_pages_es_fut_volatility_regimes() -> Result<()> {
// This test validates PAGES test on real ES.FUT data
// Expected: Detect regime changes during market open/close, news events
// Load ES.FUT data (implementation depends on available test data)
// let bars = load_dbn_test_data("ES.FUT")?;
// Initialize PAGES with parameters tuned for ES.FUT
// ES.FUT typical intraday variance: ~1.0-2.0 points²
let mut pages = PAGESTest::new(1.5, 0.5, 5.0, 20);
// Simulate ES.FUT price returns (replace with real data when available)
let simulated_returns = vec![
// Low volatility period (09:30-10:00)
0.1, -0.05, 0.08, -0.06, 0.04, 0.03, -0.02, 0.05, -0.03, 0.06,
// High volatility spike (10:00-10:30, news event)
0.8, -0.6, 0.9, -0.7, 0.85, 0.75, -0.65, 0.8, -0.5, 0.7,
];
let mut detections = Vec::new();
for (idx, &ret) in simulated_returns.iter().enumerate() {
if let Some(change) = pages.update(ret)? {
detections.push((idx, change));
println!(
"ES.FUT variance change at bar {}: ratio {:.2}x, statistic {:.2}",
idx, change.variance_ratio, change.pages_statistic
);
}
}
// Should detect volatility spike
assert!(
!detections.is_empty(),
"Should detect volatility regime change in ES.FUT"
);
// First detection should be during high volatility period (indices 10+)
assert!(
detections[0].0 >= 10,
"Should detect change during high volatility period"
);
Ok(())
}
#[test]
#[ignore = "Requires DBN test data files"]
fn test_pages_nq_fut_market_open_volatility() -> Result<()> {
// NQ.FUT typically shows volatility spike at market open (09:30 ET)
let mut pages = PAGESTest::new(2.0, 0.5, 5.0, 20);
// Simulate pre-market (low vol) → market open (high vol)
let simulated_returns = vec![
// Pre-market: low volatility
0.05, -0.03, 0.04, -0.02, 0.03, 0.02, -0.01, 0.03, -0.02, 0.04,
// Market open: volatility surge
1.5, -1.2, 1.8, -1.4, 1.6, 1.3, -1.1, 1.4, -0.9, 1.2,
];
let mut detected = false;
for (idx, &ret) in simulated_returns.iter().enumerate() {
if let Some(change) = pages.update(ret)? {
detected = true;
assert!(idx >= 10, "Should detect change during market open period");
assert!(
change.variance_ratio > 2.0,
"Market open should show significant variance increase"
);
break;
}
}
assert!(detected, "Should detect market open volatility spike");
Ok(())
}
// ============================================================================
// Performance Benchmarks
// ============================================================================
#[test]
fn test_pages_performance_latency() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
// Warmup
for i in 0..100 {
pages.update(i as f64)?;
}
// Benchmark 1000 updates
let iterations = 1000;
let start = Instant::now();
for i in 0..iterations {
pages.update((i as f64) * 0.1)?;
}
let duration = start.elapsed();
let avg_latency_us = duration.as_micros() as f64 / iterations as f64;
println!(
"PAGES update latency: {:.2}μs per update (target: <80μs)",
avg_latency_us
);
assert!(
avg_latency_us < 80.0,
"PAGES update latency {:.2}μs exceeds 80μs target",
avg_latency_us
);
Ok(())
}
#[test]
fn test_pages_memory_efficiency() {
// PAGES should use minimal memory (VecDeque + running stats)
let pages = PAGESTest::new(1.0, 0.5, 5.0, 50);
let size = std::mem::size_of_val(&pages);
println!("PAGESTest struct size: {} bytes", size);
// VecDeque overhead + running stats should be < 1KB even with window=50
assert!(
size < 1024,
"PAGESTest memory usage {} bytes exceeds 1KB",
size
);
}
// ============================================================================
// Property-Based Tests
// ============================================================================
#[test]
fn test_pages_cumulative_sum_non_negative() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
use rand_distr::{Distribution, Normal};
let normal = Normal::new(0.0, 1.5).unwrap();
let mut rng = rand::thread_rng();
for _ in 0..100 {
pages.update(normal.sample(&mut rng))?;
// Page's statistic must always be non-negative (max with 0)
assert!(
pages.get_cumulative_sum() >= 0.0,
"Cumulative sum should never be negative"
);
}
Ok(())
}
#[test]
fn test_pages_variance_always_non_negative() -> Result<()> {
let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
for i in -50..50 {
pages.update(i as f64)?;
let variance = pages.get_current_variance();
assert!(
variance >= 0.0,
"Variance should never be negative, got {}",
variance
);
}
Ok(())
}