//! Integration tests for FeatureCacheService //! //! Tests the complete feature cache workflow including: //! - Feature extraction from OHLCV bars //! - In-memory LRU caching //! - SHA-256-based invalidation //! - Cache statistics tracking use chrono::{Duration, Utc}; use ml::features::{extract_ml_features, FeatureCacheService, OHLCVBar}; fn create_test_bars(count: usize, offset: f64) -> Vec { let base_time = Utc::now(); (0..count) .map(|i| OHLCVBar { timestamp: base_time + Duration::seconds(i as i64), open: 100.0 + i as f64 + offset, high: 105.0 + i as f64 + offset, low: 95.0 + i as f64 + offset, close: 102.0 + i as f64 + offset, volume: 1000.0 + i as f64 * 10.0, }) .collect() } #[tokio::test] async fn test_feature_cache_service_disabled() { let service = FeatureCacheService::disabled(); let bars = create_test_bars(50, 0.0); // Compute features (no caching) let result = service.get_or_compute("TEST", &bars).await; assert!(result.is_ok()); let matrix = result.unwrap(); assert_eq!(matrix.sample_count, 50); assert_eq!(matrix.feature_dim, 15); // 15 core features // Verify stats let stats = service.get_stats().await; assert_eq!(stats.hits, 0); assert_eq!(stats.misses, 0); assert!(stats.features_computed > 0); } #[tokio::test] async fn test_feature_cache_hit() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); // First call - cache miss let result1 = service.get_or_compute("AAPL", &bars).await.unwrap(); assert_eq!(result1.sample_count, 50); assert_eq!(result1.feature_dim, 15); let stats1 = service.get_stats().await; assert_eq!(stats1.misses, 1); assert_eq!(stats1.hits, 0); // Second call with same data - cache hit let result2 = service.get_or_compute("AAPL", &bars).await.unwrap(); assert_eq!(result2.sample_count, 50); let stats2 = service.get_stats().await; assert_eq!(stats2.hits, 1); assert_eq!(stats2.misses, 1); // Hit rate should be 50% assert!((stats2.hit_rate() - 0.5).abs() < 0.01); } #[tokio::test] async fn test_cache_invalidation_on_data_change() { let service = FeatureCacheService::new(None, Some(10), None); let bars1 = create_test_bars(50, 0.0); // Cache features service.get_or_compute("TEST", &bars1).await.unwrap(); assert!(service.is_cached("TEST").await); // Different data with same symbol - should compute new features let bars2 = create_test_bars(50, 100.0); // Different offset let result = service.get_or_compute("TEST", &bars2).await.unwrap(); assert_eq!(result.sample_count, 50); // Should be 2 cache misses (different data) let stats = service.get_stats().await; assert_eq!(stats.misses, 2); } #[tokio::test] async fn test_explicit_invalidation() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); // Cache features service.get_or_compute("AAPL", &bars).await.unwrap(); assert!(service.is_cached("AAPL").await); // Invalidate cache service.invalidate("AAPL").await.unwrap(); assert!(!service.is_cached("AAPL").await); let stats = service.get_stats().await; assert_eq!(stats.invalidations, 1); } #[tokio::test] async fn test_multiple_symbols() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); // Cache features for multiple symbols service.get_or_compute("AAPL", &bars).await.unwrap(); service.get_or_compute("MSFT", &bars).await.unwrap(); service.get_or_compute("GOOGL", &bars).await.unwrap(); // All should be cached assert!(service.is_cached("AAPL").await); assert!(service.is_cached("MSFT").await); assert!(service.is_cached("GOOGL").await); // List cached symbols let symbols = service.list_cached_symbols().await; assert_eq!(symbols.len(), 3); assert!(symbols.contains(&"AAPL".to_string())); assert!(symbols.contains(&"MSFT".to_string())); assert!(symbols.contains(&"GOOGL".to_string())); } #[tokio::test] async fn test_lru_eviction() { // Small cache size to test eviction let service = FeatureCacheService::new(None, Some(2), None); let bars = create_test_bars(50, 0.0); // Cache 3 symbols (exceeds cache size) service.get_or_compute("AAPL", &bars).await.unwrap(); service.get_or_compute("MSFT", &bars).await.unwrap(); service.get_or_compute("GOOGL", &bars).await.unwrap(); // Cache should contain at most 2 entries let stats = service.get_stats().await; assert!(stats.cache_size <= 2); } #[tokio::test] async fn test_feature_extraction_validation() { let bars = create_test_bars(50, 0.0); let features = extract_ml_features(&bars).unwrap(); // Validate dimensions assert_eq!(features.len(), 50); for feature_vec in &features { assert_eq!(feature_vec.len(), 15); // Validate all features are finite for &value in feature_vec { assert!( value.is_finite(), "Feature should be finite, got: {}", value ); } } } #[tokio::test] async fn test_insufficient_data_error() { // Too few bars for feature extraction let bars = create_test_bars(5, 0.0); let result = extract_ml_features(&bars); assert!(result.is_err()); } #[tokio::test] async fn test_cache_statistics() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); // Perform various operations service.get_or_compute("AAPL", &bars).await.unwrap(); // miss service.get_or_compute("AAPL", &bars).await.unwrap(); // hit service.get_or_compute("MSFT", &bars).await.unwrap(); // miss service.invalidate("AAPL").await.unwrap(); // invalidation let stats = service.get_stats().await; assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 2); assert_eq!(stats.invalidations, 1); assert!(stats.features_computed > 0); assert!(stats.features_loaded > 0); // Hit rate should be 1/3 ≈ 0.333 assert!((stats.hit_rate() - 0.333).abs() < 0.01); } #[tokio::test] async fn test_data_hash_determinism() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); // Compute features twice with same data let result1 = service.get_or_compute("TEST", &bars).await.unwrap(); let result2 = service.get_or_compute("TEST", &bars).await.unwrap(); // Should get cache hit (same hash) let stats = service.get_stats().await; assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 1); // Results should be identical assert_eq!(result1.sample_count, result2.sample_count); assert_eq!(result1.feature_dim, result2.feature_dim); } #[tokio::test] async fn test_feature_matrix_validation() { let service = FeatureCacheService::new(None, Some(10), None); let bars = create_test_bars(50, 0.0); let matrix = service.get_or_compute("TEST", &bars).await.unwrap(); // Validate matrix assert!(matrix.validate().is_ok()); assert_eq!(matrix.symbol, "TEST"); assert_eq!(matrix.sample_count, 50); assert_eq!(matrix.feature_dim, 15); }