WAVE 21: Core type definitions and trainer configs updated Files Modified (13 files): - ml/src/features/extraction.rs: FeatureVector = [f64; 54] - common/src/features/types.rs: Added FeatureVector54 - ml/src/trainers/dqn.rs: state_dim 225→54 - ml/src/trainers/ppo.rs: state_dim 225→54 - ml/src/dqn/dqn.rs, config.rs, replay_buffer.rs: Updated configs - ml/src/hyperopt/adapters/: All adapters updated to 54-dim - ml/src/features/unified.rs: Struct fields updated - ml/src/trainers/tft_parquet.rs: Return types updated Agents Deployed: 5 parallel agents Test Results: cargo check --package ml --lib PASSING Next: Wave 2 (examples), Wave 3 (tests), Wave 4 (OFI integration) Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
247 lines
7.9 KiB
Rust
247 lines
7.9 KiB
Rust
//! Regression Tests for 46-Feature Extraction
|
|
//!
|
|
//! This test suite validates feature extraction consistency against
|
|
//! known-good values and ensures backward compatibility where applicable.
|
|
|
|
#![allow(unused_crate_dependencies)]
|
|
|
|
use ml::features::extraction::{extract_ml_features, OHLCVBar};
|
|
use anyhow::Result;
|
|
use chrono::{Duration, Utc};
|
|
|
|
/// Test: Known-good feature values
|
|
#[test]
|
|
fn test_known_good_feature_values() -> Result<()> {
|
|
// OBJECTIVE: Verify features match expected values for synthetic data
|
|
// EXPECTED: Features for specific bars match pre-computed values
|
|
|
|
let bars = create_synthetic_bars_with_known_features()?;
|
|
let features = extract_ml_features(&bars)?;
|
|
|
|
assert!(!features.is_empty(), "Should extract features");
|
|
|
|
// First feature vector (bar 50, first after warmup)
|
|
let fv = &features[0];
|
|
|
|
// Verify we extracted 46 features
|
|
assert_eq!(fv.len(), 46, "Should have 46 features");
|
|
|
|
// Verify all features are finite
|
|
for (i, &value) in fv.iter().enumerate() {
|
|
assert!(value.is_finite(), "Feature {} is not finite", i);
|
|
}
|
|
|
|
// Verify expected ranges
|
|
// OHLCV (0-4): log returns should be small
|
|
for i in 0..4 {
|
|
assert!(fv[i].abs() < 1.0, "OHLCV feature {} out of range: {}", i, fv[i]);
|
|
}
|
|
|
|
println!("✅ Known-good feature values verified");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: Consistency across multiple runs
|
|
#[test]
|
|
fn test_extraction_consistency() -> Result<()> {
|
|
// OBJECTIVE: Verify extraction is deterministic
|
|
// EXPECTED: Same input produces identical output across runs
|
|
|
|
let bars = create_test_bars(100)?;
|
|
|
|
let features1 = extract_ml_features(&bars)?;
|
|
let features2 = extract_ml_features(&bars)?;
|
|
let features3 = extract_ml_features(&bars)?;
|
|
|
|
assert_eq!(features1.len(), features2.len());
|
|
assert_eq!(features2.len(), features3.len());
|
|
|
|
// Compare values
|
|
for (idx, (fv1, fv2)) in features1.iter().zip(features2.iter()).enumerate() {
|
|
for (feat_idx, (&v1, &v2)) in fv1.iter().zip(fv2.iter()).enumerate() {
|
|
assert_eq!(v1, v2,
|
|
"Inconsistency at bar {} feature {}: {} vs {}", idx, feat_idx, v1, v2);
|
|
}
|
|
}
|
|
|
|
for (idx, (fv2, fv3)) in features2.iter().zip(features3.iter()).enumerate() {
|
|
for (feat_idx, (&v2, &v3)) in fv2.iter().zip(fv3.iter()).enumerate() {
|
|
assert_eq!(v2, v3,
|
|
"Inconsistency across runs at bar {} feature {}: {} vs {}", idx, feat_idx, v2, v3);
|
|
}
|
|
}
|
|
|
|
println!("✅ Extraction is fully deterministic across runs");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: Feature count consistency
|
|
#[test]
|
|
fn test_feature_count_consistency() -> Result<()> {
|
|
// OBJECTIVE: Verify correct number of features extracted
|
|
// EXPECTED: Always 46 features per bar, always (bars - 50) vectors
|
|
|
|
let test_counts = vec![60, 100, 200, 500];
|
|
|
|
for bar_count in test_counts {
|
|
let bars = create_test_bars(bar_count)?;
|
|
let features = extract_ml_features(&bars)?;
|
|
|
|
let expected_count = bar_count - 50; // Warmup period
|
|
assert_eq!(features.len(), expected_count,
|
|
"Expected {} vectors with {} bars, got {}",
|
|
expected_count, bar_count, features.len());
|
|
|
|
// Check all vectors have 46 features
|
|
for (i, fv) in features.iter().enumerate() {
|
|
assert_eq!(fv.len(), 46,
|
|
"Vector {} has {} features, expected 46", i, fv.len());
|
|
}
|
|
}
|
|
|
|
println!("✅ Feature count is consistent across input sizes");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: Feature range consistency
|
|
#[test]
|
|
fn test_feature_range_consistency() -> Result<()> {
|
|
// OBJECTIVE: Verify features stay in expected ranges consistently
|
|
// EXPECTED: All features within [-1000, +1000] across all test data
|
|
|
|
let bars = create_test_bars(200)?;
|
|
let features = extract_ml_features(&bars)?;
|
|
|
|
let mut min_per_feature = vec![f64::INFINITY; 46];
|
|
let mut max_per_feature = vec![f64::NEG_INFINITY; 46];
|
|
|
|
// Collect min/max per feature
|
|
for fv in &features {
|
|
for (i, &value) in fv.iter().enumerate() {
|
|
min_per_feature[i] = min_per_feature[i].min(value);
|
|
max_per_feature[i] = max_per_feature[i].max(value);
|
|
}
|
|
}
|
|
|
|
// Verify ranges are reasonable
|
|
for i in 0..46 {
|
|
let min_val = min_per_feature[i];
|
|
let max_val = max_per_feature[i];
|
|
let range = max_val - min_val;
|
|
|
|
assert!(min_val > -1000.0 && min_val < 1000.0,
|
|
"Feature {} min value out of range: {}", i, min_val);
|
|
assert!(max_val > -1000.0 && max_val < 1000.0,
|
|
"Feature {} max value out of range: {}", i, max_val);
|
|
}
|
|
|
|
println!("✅ Feature ranges consistent across test data");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: Empty and minimal data handling
|
|
#[test]
|
|
fn test_edge_case_data_handling() -> Result<()> {
|
|
// OBJECTIVE: Verify proper error handling for edge cases
|
|
// EXPECTED: Appropriate errors for invalid input
|
|
|
|
// Empty bars
|
|
let empty_bars: Vec<OHLCVBar> = vec![];
|
|
assert!(extract_ml_features(&empty_bars).is_err(),
|
|
"Should error on empty input");
|
|
|
|
// Insufficient bars (< 50)
|
|
let insufficient = create_test_bars(30)?;
|
|
assert!(extract_ml_features(&insufficient).is_err(),
|
|
"Should error on < 50 bars");
|
|
|
|
// Exactly 50 bars (warmup only)
|
|
let exact_warmup = create_test_bars(50)?;
|
|
let result = extract_ml_features(&exact_warmup)?;
|
|
assert_eq!(result.len(), 0, "Should produce 0 features with exactly 50 bars");
|
|
|
|
// 51 bars (minimal extract)
|
|
let minimal_extract = create_test_bars(51)?;
|
|
let result = extract_ml_features(&minimal_extract)?;
|
|
assert_eq!(result.len(), 1, "Should produce 1 feature with 51 bars");
|
|
|
|
println!("✅ Edge case data handling correct");
|
|
Ok(())
|
|
}
|
|
|
|
/// Test: OHLCV category validation
|
|
#[test]
|
|
fn test_ohlcv_category_consistency() -> Result<()> {
|
|
// OBJECTIVE: Verify OHLCV (indices 0-4) are extracted consistently
|
|
// EXPECTED: Log returns computed from actual OHLCV values
|
|
|
|
let bars = create_test_bars(100)?;
|
|
let features = extract_ml_features(&bars)?;
|
|
|
|
// All OHLCV features should be present and finite
|
|
for fv in features.iter().take(10) {
|
|
for i in 0..5 {
|
|
assert!(fv[i].is_finite(), "OHLCV feature {} not finite", i);
|
|
}
|
|
}
|
|
|
|
// Verify OHLCV features make sense:
|
|
// - Log returns should reflect price movements
|
|
// - Volume ratio should be positive
|
|
for fv in features.iter().take(10) {
|
|
let volume_ratio = fv[4];
|
|
assert!(volume_ratio >= 0.0 || volume_ratio.is_nan(),
|
|
"Volume ratio should be non-negative: {}", volume_ratio);
|
|
}
|
|
|
|
println!("✅ OHLCV category consistent");
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
fn create_test_bars(count: usize) -> Result<Vec<OHLCVBar>> {
|
|
let mut bars = Vec::with_capacity(count);
|
|
let mut timestamp = Utc::now();
|
|
let mut price = 4500.0;
|
|
|
|
for _ in 0..count {
|
|
let bar = OHLCVBar {
|
|
timestamp,
|
|
open: price,
|
|
high: price + 2.0,
|
|
low: price - 2.0,
|
|
close: price + 1.0,
|
|
volume: 1000.0,
|
|
};
|
|
bars.push(bar);
|
|
|
|
timestamp = timestamp + Duration::minutes(1);
|
|
price += (rand::random::<f64>() - 0.5) * 2.0;
|
|
}
|
|
|
|
Ok(bars)
|
|
}
|
|
|
|
fn create_synthetic_bars_with_known_features() -> Result<Vec<OHLCVBar>> {
|
|
// Create 60 synthetic bars with predictable feature values
|
|
let mut bars = Vec::new();
|
|
let mut timestamp = Utc::now();
|
|
|
|
for i in 0..60 {
|
|
bars.push(OHLCVBar {
|
|
timestamp,
|
|
open: 4500.0 + (i as f64) * 0.5,
|
|
high: 4502.0 + (i as f64) * 0.5,
|
|
low: 4498.0 + (i as f64) * 0.5,
|
|
close: 4500.0 + (i as f64) * 0.5,
|
|
volume: 1000.0,
|
|
});
|
|
timestamp = timestamp + Duration::minutes(1);
|
|
}
|
|
|
|
Ok(bars)
|
|
}
|