From 74a595aff809f175780405b5a04dbbe59d062a37 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 12 Mar 2026 20:06:17 +0100 Subject: [PATCH] fix(tests): update stale 51-dim feature assertions to match 42-dim extractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProductionFeatureExtractorAdapter was changed to produce 42 features (40 base + 2 regime) but three test sites and the FEATURE_NAMES constant still expected 51 (42 + 1 volatility_regime + 8 OFI placeholders). - backtesting: strategy_runner test assertions 51→42 - trading-service: ensemble_coordinator test assertions 51→42 - trading-service: FEATURE_NAMES_51 → FEATURE_NAMES_42 (drop OFI placeholders and volatility_regime, matching extraction.rs v2 layout) Co-Authored-By: Claude Opus 4.6 --- crates/backtesting/src/strategy_runner.rs | 10 ++-- .../src/ensemble_coordinator.rs | 48 +++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/backtesting/src/strategy_runner.rs b/crates/backtesting/src/strategy_runner.rs index 7a3ad85ef..14189d442 100644 --- a/crates/backtesting/src/strategy_runner.rs +++ b/crates/backtesting/src/strategy_runner.rs @@ -1336,7 +1336,7 @@ mod tests { } #[test] - fn test_production_features_have_51_dimensions() -> Result<(), Box> { + fn test_production_features_have_42_dimensions() -> Result<(), Box> { let mut extractor = ProductionFeatureExtractorAdapter::new(); // Feed 55 price updates (past the warmup period of 50) for i in 0..55 { @@ -1348,8 +1348,8 @@ mod tests { let features = extractor.extract_features()?; assert_eq!( features.len(), - 51, - "Production extractor should produce exactly 51 features, got {}", + 42, + "Production extractor should produce exactly 42 features, got {}", features.len() ); // Validate all features are finite @@ -1374,10 +1374,10 @@ mod tests { // Should either return an error or return a short vector // (during warmup, extraction may fail) let result = extractor.extract_features(); - // We accept either an error (warmup not ready) or a valid 51-dim result + // We accept either an error (warmup not ready) or a valid 42-dim result match result { Ok(features) => { - assert_eq!(features.len(), 51); + assert_eq!(features.len(), 42); } Err(_) => { // Expected during warmup - this is fine diff --git a/services/trading_service/src/ensemble_coordinator.rs b/services/trading_service/src/ensemble_coordinator.rs index 08c6eea84..bf4438c17 100644 --- a/services/trading_service/src/ensemble_coordinator.rs +++ b/services/trading_service/src/ensemble_coordinator.rs @@ -50,18 +50,17 @@ use tokio::sync::RwLock; use tracing::{debug, info, warn}; use uuid::Uuid; -/// Canonical feature names for the 51-dimension production feature vector. +/// Canonical feature names for the 42-dimension production feature vector. /// -/// Layout matches `ml::features::extraction::FeatureExtractor`: +/// Layout matches `ml::features::extraction::FeatureExtractor::extract_current_features_v2`: /// - 0..4: OHLCV (5) /// - 5..9: Technical indicators (5) /// - 10..15: Price patterns (6) /// - 16..21: Volume features (6) /// - 22..26: Time features (5) /// - 27..39: Statistical features (13) -/// - 40..42: Regime detection (3) -/// - 43..50: OFI placeholders (8) -const FEATURE_NAMES_51: &[&str] = &[ +/// - 40..41: Regime detection (2) +const FEATURE_NAMES_42: &[&str] = &[ // OHLCV (5) "open_return", "high_return", "low_return", "close_return", "volume_norm", // Technical indicators (5) @@ -80,11 +79,8 @@ const FEATURE_NAMES_51: &[&str] = &[ "autocorr_lag1", "autocorr_lag5", "autocorr_lag10", "skewness_5", "skewness_10", "skewness_20", "kurtosis_5", "kurtosis_10", "kurtosis_20", - // Regime detection (3) - "adx_strength", "cusum_direction", "volatility_regime", - // OFI placeholders (8) - "ofi_level1", "ofi_level5", "depth_imbalance", "vpin", - "kyle_lambda", "bid_slope", "ask_slope", "trade_imbalance", + // Regime detection (2) + "adx_strength", "cusum_direction", ]; /// Minimum number of market data updates required before reliable feature extraction. @@ -679,17 +675,17 @@ impl EnsembleCoordinator { if extractors.contains_key(symbol) { 1 } else { 0 } } - /// Extract real 51-dimensional features for `symbol` using the production + /// Extract real 42-dimensional features for `symbol` using the production /// feature extractor from the `ml` crate. /// /// If the extractor for this symbol has not received enough warmup bars - /// (minimum ~51 updates), extraction will fail gracefully and a zero-filled + /// (minimum ~50 updates), extraction will fail gracefully and a zero-filled /// feature vector is returned with a warning log so the ensemble can still /// produce a low-confidence prediction. pub async fn fetch_features_for_symbol(&self, symbol: &str) -> Result { use common::ml_strategy::ProductionFeatureExtractor225; - let names: Vec = FEATURE_NAMES_51.iter().map(|s| (*s).to_string()).collect(); + let names: Vec = FEATURE_NAMES_42.iter().map(|s| (*s).to_string()).collect(); let mut extractors = self.feature_extractors.write().await; @@ -720,7 +716,7 @@ impl EnsembleCoordinator { } // Fallback: zeros so the ensemble can still run (will produce low confidence). - Ok(Features::new(vec![0.0; FEATURE_NAMES_51.len()], names) + Ok(Features::new(vec![0.0; FEATURE_NAMES_42.len()], names) .with_symbol(symbol.to_string())) } } @@ -1102,14 +1098,14 @@ mod tests { async fn test_fetch_features_returns_zeros_without_warmup() { let coordinator = EnsembleCoordinator::new(); - // No market data fed yet — should return zero-filled 51-dim vector + // No market data fed yet — should return zero-filled 42-dim vector let features = coordinator .fetch_features_for_symbol("ES.FUT") .await .expect("fetch_features_for_symbol should not fail"); - assert_eq!(features.values.len(), 51, "Should return 51 features"); - assert_eq!(features.names.len(), 51, "Should have 51 feature names"); + assert_eq!(features.values.len(), 42, "Should return 42 features"); + assert_eq!(features.names.len(), 42, "Should have 42 feature names"); assert_eq!(features.symbol, Some("ES.FUT".to_string())); // All values should be zero (no data fed) @@ -1144,14 +1140,14 @@ mod tests { .await .expect("fetch_features_for_symbol should succeed after warmup"); - assert_eq!(features.values.len(), 51, "Should return 51-dim vector"); + assert_eq!(features.values.len(), 42, "Should return 42-dim vector"); assert_eq!(features.symbol, Some("ES.FUT".to_string())); // After warmup, at least some features should be non-zero let non_zero_count = features.values.iter().filter(|v| v.abs() > 1e-12).count(); assert!( non_zero_count > 5, - "Expected many non-zero features after warmup, got {} non-zero out of 51", + "Expected many non-zero features after warmup, got {} non-zero out of 42", non_zero_count, ); @@ -1168,15 +1164,15 @@ mod tests { } #[tokio::test] - async fn test_feature_names_match_51_dim_layout() { - // Verify the constant array has exactly 51 entries - assert_eq!(FEATURE_NAMES_51.len(), 51, "FEATURE_NAMES_51 must have 51 entries"); + async fn test_feature_names_match_42_dim_layout() { + // Verify the constant array has exactly 42 entries + assert_eq!(FEATURE_NAMES_42.len(), 42, "FEATURE_NAMES_42 must have 42 entries"); // Spot-check a few known names - assert_eq!(FEATURE_NAMES_51[0], "open_return"); - assert_eq!(FEATURE_NAMES_51[5], "rsi_14"); - assert_eq!(FEATURE_NAMES_51[43], "ofi_level1"); - assert_eq!(FEATURE_NAMES_51[50], "trade_imbalance"); + assert_eq!(FEATURE_NAMES_42[0], "open_return"); + assert_eq!(FEATURE_NAMES_42[5], "rsi_14"); + assert_eq!(FEATURE_NAMES_42[40], "adx_strength"); + assert_eq!(FEATURE_NAMES_42[41], "cusum_direction"); } #[tokio::test]