fix(tests): update stale 51-dim feature assertions to match 42-dim extractor

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 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-12 20:06:17 +01:00
parent 68eefbfc9e
commit 74a595aff8
2 changed files with 27 additions and 31 deletions

View File

@@ -1336,7 +1336,7 @@ mod tests {
}
#[test]
fn test_production_features_have_51_dimensions() -> Result<(), Box<dyn std::error::Error>> {
fn test_production_features_have_42_dimensions() -> Result<(), Box<dyn std::error::Error>> {
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

View File

@@ -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<Features> {
use common::ml_strategy::ProductionFeatureExtractor225;
let names: Vec<String> = FEATURE_NAMES_51.iter().map(|s| (*s).to_string()).collect();
let names: Vec<String> = 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]