test(trading_service): enhanced ML service component tests
Add 16 unit tests for Enhanced ML service components: - EnsembleConfig::default() values (min_models, thresholds, voting) - FeaturePreprocessor::classify_feature_type() for price, volume, technical, sentiment, and unknown features - FeaturePreprocessor normalization (z-score, tanh fallback, zero std_dev) - FeaturePreprocessor default stats validation - RuntimeModelInfo creation for all 5 model types (DQN/PPO/TFT/Mamba/LNN) - ModelPerformanceMetrics::default() zero initialization - FeatureNormStats volatility default bounds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2075,3 +2075,245 @@ mod feature_preprocessor_tests {
|
||||
assert!(result >= -10.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod enhanced_ml_tests {
|
||||
use super::*;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 1. EnsembleConfig::default() values
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_ensemble_config_defaults() {
|
||||
let config = EnsembleConfig::default();
|
||||
assert_eq!(config.min_models, 2, "min_models should default to 2");
|
||||
assert!(
|
||||
(config.confidence_threshold - 0.7).abs() < 1e-10,
|
||||
"confidence_threshold should default to 0.7"
|
||||
);
|
||||
assert!(
|
||||
config.use_weighted_voting,
|
||||
"use_weighted_voting should default to true"
|
||||
);
|
||||
assert_eq!(
|
||||
config.fallback_timeout_ms, 50,
|
||||
"fallback_timeout_ms should default to 50"
|
||||
);
|
||||
assert!(
|
||||
(config.consensus_threshold - 0.6).abs() < 1e-10,
|
||||
"consensus_threshold should default to 0.6"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 2. FeaturePreprocessor::classify_feature_type
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_classify_price_features() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
assert_eq!(pp.classify_feature_type("price_momentum") as i32, FeatureType::Price as i32);
|
||||
assert_eq!(pp.classify_feature_type("close_price") as i32, FeatureType::Price as i32);
|
||||
assert_eq!(pp.classify_feature_type("momentum_5m") as i32, FeatureType::Price as i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_volume_features() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
assert_eq!(pp.classify_feature_type("volume") as i32, FeatureType::Volume as i32);
|
||||
assert_eq!(pp.classify_feature_type("volume_ratio") as i32, FeatureType::Volume as i32);
|
||||
assert_eq!(pp.classify_feature_type("orderbook_depth") as i32, FeatureType::Volume as i32);
|
||||
assert_eq!(pp.classify_feature_type("depth_imbalance") as i32, FeatureType::Volume as i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_technical_features() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
assert_eq!(pp.classify_feature_type("volatility") as i32, FeatureType::Technical as i32);
|
||||
assert_eq!(pp.classify_feature_type("rsi_14") as i32, FeatureType::Technical as i32);
|
||||
assert_eq!(pp.classify_feature_type("ma_20") as i32, FeatureType::Technical as i32);
|
||||
assert_eq!(pp.classify_feature_type("spread_bps") as i32, FeatureType::Technical as i32);
|
||||
assert_eq!(pp.classify_feature_type("liquidity_score") as i32, FeatureType::Technical as i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_sentiment_features() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
assert_eq!(pp.classify_feature_type("sentiment_score") as i32, FeatureType::Sentiment as i32);
|
||||
assert_eq!(pp.classify_feature_type("news_impact") as i32, FeatureType::Sentiment as i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_unknown_defaults_to_technical() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
assert_eq!(pp.classify_feature_type("foo_bar_baz") as i32, FeatureType::Technical as i32);
|
||||
assert_eq!(pp.classify_feature_type("") as i32, FeatureType::Technical as i32);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 3. FeaturePreprocessor normalization (z-score, tanh fallback)
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_normalize_known_feature_zscore() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
// price_momentum: mean=0.0, std_dev=0.1
|
||||
// z-score for value=0.05: (0.05 - 0.0) / 0.1 = 0.5
|
||||
let result = pp.normalize("price_momentum", 0.05);
|
||||
assert!((result - 0.5).abs() < 1e-10, "Expected 0.5; got {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_volume_zscore() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
// volume: mean=1_000_000, std_dev=500_000
|
||||
// z-score for value=1_500_000: (1_500_000 - 1_000_000) / 500_000 = 1.0
|
||||
let result = pp.normalize("volume", 1_500_000.0);
|
||||
assert!((result - 1.0).abs() < 1e-10, "Expected 1.0; got {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_unknown_feature_uses_tanh() {
|
||||
let pp = FeaturePreprocessor::new();
|
||||
// Unknown feature uses value.tanh()
|
||||
let value: f64 = 0.5;
|
||||
let expected = value.tanh();
|
||||
let result = pp.normalize("unknown_feature_xyz", value);
|
||||
assert!((result - expected).abs() < 1e-10, "Expected tanh({})={}; got {}", value, expected, result);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 4. FeaturePreprocessor default stats
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_preprocessor_default_has_three_features() {
|
||||
let pp = FeaturePreprocessor::default();
|
||||
assert_eq!(pp.stats.len(), 3, "Default preprocessor should have 3 feature stats");
|
||||
assert!(pp.stats.contains_key("price_momentum"));
|
||||
assert!(pp.stats.contains_key("volume"));
|
||||
assert!(pp.stats.contains_key("volatility"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preprocessor_new_equals_default() {
|
||||
let pp_new = FeaturePreprocessor::new();
|
||||
let pp_default = FeaturePreprocessor::default();
|
||||
assert_eq!(pp_new.stats.len(), pp_default.stats.len());
|
||||
for (key, new_stat) in &pp_new.stats {
|
||||
let default_stat = pp_default.stats.get(key).unwrap();
|
||||
assert!((new_stat.mean - default_stat.mean).abs() < 1e-10);
|
||||
assert!((new_stat.std_dev - default_stat.std_dev).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 5. RuntimeModelInfo creation
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_runtime_model_info_creation() {
|
||||
let info = RuntimeModelInfo {
|
||||
model_id: "test-dqn-v1".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
load_time: SystemTime::now(),
|
||||
last_inference: None,
|
||||
inference_count: 0,
|
||||
error_count: 0,
|
||||
avg_latency_us: 0.0,
|
||||
confidence_threshold: 0.7,
|
||||
weight_in_ensemble: 1.0,
|
||||
fallback_priority: 0,
|
||||
model_type: ModelType::DQN,
|
||||
supported_symbols: vec!["EURUSD".to_string(), "GBPUSD".to_string()],
|
||||
supported_horizons: vec![1, 5, 15],
|
||||
feature_count: 16,
|
||||
model_instance: None,
|
||||
};
|
||||
|
||||
assert_eq!(info.model_id, "test-dqn-v1");
|
||||
assert_eq!(info.model_type, ModelType::DQN);
|
||||
assert_eq!(info.feature_count, 16);
|
||||
assert_eq!(info.supported_symbols.len(), 2);
|
||||
assert_eq!(info.supported_horizons.len(), 3);
|
||||
assert!(info.last_inference.is_none());
|
||||
assert!(info.model_instance.is_none());
|
||||
assert_eq!(info.inference_count, 0);
|
||||
assert_eq!(info.error_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_runtime_model_info_various_model_types() {
|
||||
let model_types = vec![
|
||||
(ModelType::DQN, "DQN"),
|
||||
(ModelType::PPO, "PPO"),
|
||||
(ModelType::TFT, "TFT"),
|
||||
(ModelType::Mamba, "Mamba"),
|
||||
(ModelType::LNN, "LNN"),
|
||||
];
|
||||
|
||||
for (model_type, name) in model_types {
|
||||
let info = RuntimeModelInfo {
|
||||
model_id: format!("test-{}", name),
|
||||
version: "1.0.0".to_string(),
|
||||
load_time: SystemTime::now(),
|
||||
last_inference: None,
|
||||
inference_count: 0,
|
||||
error_count: 0,
|
||||
avg_latency_us: 0.0,
|
||||
confidence_threshold: 0.7,
|
||||
weight_in_ensemble: 0.25,
|
||||
fallback_priority: 1,
|
||||
model_type,
|
||||
supported_symbols: vec![],
|
||||
supported_horizons: vec![],
|
||||
feature_count: 16,
|
||||
model_instance: None,
|
||||
};
|
||||
assert_eq!(info.model_type, model_type, "Model type mismatch for {}", name);
|
||||
assert_eq!(info.weight_in_ensemble, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 6. ModelPerformanceMetrics defaults
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_model_performance_metrics_default() {
|
||||
let metrics = ModelPerformanceMetrics::default();
|
||||
assert_eq!(metrics.total_predictions, 0);
|
||||
assert_eq!(metrics.successful_predictions, 0);
|
||||
assert_eq!(metrics.failed_predictions, 0);
|
||||
assert!((metrics.avg_latency_us - 0.0).abs() < 1e-10);
|
||||
assert!((metrics.p95_latency_us - 0.0).abs() < 1e-10);
|
||||
assert!((metrics.accuracy_percentage - 0.0).abs() < 1e-10);
|
||||
assert!(metrics.last_health_check.is_none());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 7. FeatureNormStats bounds validation
|
||||
// -----------------------------------------------------------------------
|
||||
#[test]
|
||||
fn test_feature_norm_stats_volatility_defaults() {
|
||||
let pp = FeaturePreprocessor::default();
|
||||
let vol_stats = pp.stats.get("volatility").unwrap();
|
||||
assert!((vol_stats.mean - 0.02).abs() < 1e-10, "Volatility mean should be 0.02");
|
||||
assert!((vol_stats.std_dev - 0.01).abs() < 1e-10, "Volatility std_dev should be 0.01");
|
||||
assert!((vol_stats.min - 0.0).abs() < 1e-10, "Volatility min should be 0.0");
|
||||
assert!((vol_stats.max - 0.5).abs() < 1e-10, "Volatility max should be 0.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_zero_std_dev_returns_raw() {
|
||||
// Create a feature with zero std_dev -- should return value as-is
|
||||
let mut pp = FeaturePreprocessor::new();
|
||||
pp.stats.insert(
|
||||
"zero_std".to_string(),
|
||||
FeatureNormStats {
|
||||
mean: 5.0,
|
||||
std_dev: 0.0,
|
||||
min: 0.0,
|
||||
max: 10.0,
|
||||
},
|
||||
);
|
||||
let result = pp.normalize("zero_std", 7.0);
|
||||
assert!((result - 7.0).abs() < 1e-10, "Zero std_dev should return raw value; got {}", result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user