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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user