Files
foxhunt/crates/ml/tests/test_extract_256_dim_features.rs
jgrusewski ca4c38d921 fix(tests): CI GPU test stability, walltime reduction, BF16 tolerance
- 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>
2026-03-15 12:00:13 +01:00

271 lines
8.2 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,
)]
//! Integration test for 54-dimension feature extraction
//!
//! Tests the extract_ml_features() function with real OHLCV data
use chrono::Utc;
use ml::features::extraction::{extract_ml_features, OHLCVBar};
use tracing::info;
use tracing::warn;
#[test]
fn test_extract_256_dim_features() {
// Create synthetic OHLCV bars (100 bars to exceed warmup period of 50)
let bars: Vec<OHLCVBar> = (0..100)
.map(|i| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i),
open: 4500.0 + i as f64 * 0.5,
high: 4510.0 + i as f64 * 0.5,
low: 4490.0 + i as f64 * 0.5,
close: 4505.0 + i as f64 * 0.5,
volume: 10000.0 + i as f64 * 100.0,
})
.collect();
// Extract features
let result = extract_ml_features(&bars);
assert!(
result.is_ok(),
"Feature extraction failed: {:?}",
result.err()
);
let features = result.unwrap();
// Should return features for bars after warmup period (100 - 50 = 50)
assert_eq!(
features.len(),
50,
"Expected 50 feature vectors (100 bars - 50 warmup), got {}",
features.len()
);
// Each feature vector should be exactly 54 dimensions
for (i, feature_vec) in features.iter().enumerate() {
assert_eq!(
feature_vec.len(),
54,
"Feature vector {} has wrong dimension: {}",
i,
feature_vec.len()
);
// Validate no NaN/Inf values
for (j, &val) in feature_vec.iter().enumerate() {
assert!(
val.is_finite(),
"Feature vector {} has non-finite value at index {}: {}",
i,
j,
val
);
}
}
info!(count = features.len(), "Successfully extracted 54-dim feature vectors");
info!(first_10 = ?&features[0][0..10], "First feature vector sample");
}
#[test]
fn test_feature_dimensions() {
// Create 60 bars (10 above minimum warmup)
let bars: Vec<OHLCVBar> = (0..60)
.map(|i| {
OHLCVBar {
timestamp: Utc::now() + chrono::Duration::minutes(i),
open: 4500.0,
high: 4510.0,
low: 4490.0,
close: 4505.0 + (i as f64 * 0.1).sin() * 5.0, // Add some variation
volume: 10000.0,
}
})
.collect();
let features = extract_ml_features(&bars).unwrap();
// Should have 10 feature vectors (60 - 50 warmup)
assert_eq!(features.len(), 10);
// Check output shape (num_bars, 54)
assert_eq!(features.len(), 10, "Wrong number of bars");
for feature_vec in &features {
assert_eq!(feature_vec.len(), 54, "Wrong feature dimension");
}
// Validate no NaN/Inf
for feature_vec in &features {
for &val in feature_vec.iter() {
assert!(val.is_finite(), "Found non-finite value: {}", val);
}
}
info!(bars = features.len(), "Feature dimensions validated: bars x 54 features");
}
#[test]
fn test_insufficient_data_error() {
// Create only 10 bars (below 50 warmup requirement)
let bars: Vec<OHLCVBar> = (0..10)
.map(|i| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i),
open: 4500.0,
high: 4510.0,
low: 4490.0,
close: 4505.0,
volume: 10000.0,
})
.collect();
let result = extract_ml_features(&bars);
assert!(result.is_err(), "Should fail with insufficient data");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Insufficient data"),
"Expected 'Insufficient data' error, got: {}",
error_msg
);
info!("Insufficient data error handled correctly");
}
#[test]
fn test_feature_normalization() {
// Create bars with extreme values to test normalization
let bars: Vec<OHLCVBar> = (0..100)
.map(|i| {
OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i),
open: 4500.0 + i as f64 * 10.0, // Large price changes
high: 4600.0 + i as f64 * 10.0,
low: 4400.0 + i as f64 * 10.0,
close: 4500.0 + i as f64 * 10.0,
volume: 100000.0 + i as f64 * 5000.0, // Large volume changes
}
})
.collect();
let features = extract_ml_features(&bars).unwrap();
// Check that features are reasonably normalized
for (i, feature_vec) in features.iter().enumerate() {
for (j, &val) in feature_vec.iter().enumerate() {
// Most features should be in reasonable range (not all, but most)
// This is a sanity check, not strict validation
if !(-10.0..=10.0).contains(&val) {
// Log but don't fail - some features may legitimately be outside this range
warn!(feature_idx = j, vector_idx = i, value = val, "Feature value outside [-10, 10]");
}
}
}
info!("Feature normalization validated");
}
#[test]
fn test_feature_consistency() {
// Test that same input produces same output (deterministic)
let bars: Vec<OHLCVBar> = (0..100)
.map(|i| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i),
open: 4500.0,
high: 4510.0,
low: 4490.0,
close: 4505.0,
volume: 10000.0,
})
.collect();
let features1 = extract_ml_features(&bars).unwrap();
let features2 = extract_ml_features(&bars).unwrap();
assert_eq!(features1.len(), features2.len());
for (vec1, vec2) in features1.iter().zip(features2.iter()) {
for (&val1, &val2) in vec1.iter().zip(vec2.iter()) {
assert!(
(val1 - val2).abs() < 1e-10,
"Features not consistent: {} vs {}",
val1,
val2
);
}
}
info!("Feature extraction is deterministic");
}