//! # Inference Engine //! //! High-performance inference engine with ONNX Runtime integration //! and optimized model serving for different latency requirements. use std::collections::HashMap; use std::sync::Arc; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use tracing::{info, warn}; // Placeholder types for ONNX Runtime (removed from dependencies) #[derive(Debug)] pub struct Environment; #[derive(Debug)] pub struct Session; use super::{InferencePriority, IntegrationHubConfig}; use crate::{InferenceResult, MLError, ModelMetadata, ModelType}; /// Configuration for fallback prediction to eliminate dangerous hardcoded values #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FallbackPredictionConfig { /// Base prediction value (replaces hardcoded 0.5) pub base_prediction: f32, /// Neutral prediction for empty features pub neutral_prediction: f64, /// Default confidence when using fallback pub default_confidence: f64, /// Signal weights for market features pub signal_weights: SignalWeights, /// Signal scaling factors pub signal_scaling: SignalScaling, /// Feature bounds for safety pub feature_bounds: FeatureBounds, /// Default feature values pub feature_defaults: FeatureDefaults, /// Prediction output bounds pub prediction_bounds: PredictionBounds, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignalWeights { pub momentum_weight: f32, pub volume_weight: f32, pub spread_weight: f32, pub volatility_weight: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignalScaling { pub momentum_scale: f32, pub volume_scale: f32, pub spread_scale: f32, pub volatility_scale: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureBounds { pub momentum_min: f32, pub momentum_max: f32, pub volume_min: f32, pub volume_max: f32, pub spread_min: f32, pub spread_max: f32, pub volatility_min: f32, pub volatility_max: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeatureDefaults { pub momentum_default: f32, pub volume_default: f32, pub spread_default: f32, pub volatility_default: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PredictionBounds { pub min: f32, pub max: f32, } impl Default for FallbackPredictionConfig { fn default() -> Self { Self::emergency_safe_defaults() } } impl FallbackPredictionConfig { /// Emergency safe defaults - ultra-conservative to prevent trading disasters pub fn emergency_safe_defaults() -> Self { tracing::warn!("Using emergency fallback prediction defaults - check config system!"); Self { base_prediction: 0.5, // Market neutral neutral_prediction: 0.5, // Market neutral default_confidence: 0.1, // Very low confidence for safety signal_weights: SignalWeights { momentum_weight: 0.05, // Very low weight to prevent strong signals volume_weight: 0.02, spread_weight: 0.01, volatility_weight: 0.01, }, signal_scaling: SignalScaling { momentum_scale: 0.01, // Very small scaling volume_scale: 0.005, spread_scale: 0.002, volatility_scale: 0.002, }, feature_bounds: FeatureBounds { momentum_min: -0.1, momentum_max: 0.1, volume_min: 0.0, volume_max: 2.0, spread_min: 0.0, spread_max: 0.1, volatility_min: 0.0, volatility_max: 0.5, }, feature_defaults: FeatureDefaults { momentum_default: 0.0, // Neutral volume_default: 1.0, // Average volume spread_default: 0.01, // Small spread volatility_default: 0.1, // Low volatility }, prediction_bounds: PredictionBounds { min: 0.45, // Very narrow range around neutral max: 0.55, }, } } } /// Configuration for inference engine #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InferenceEngineConfig { /// Maximum concurrent inference requests pub max_concurrent_requests: usize, /// Default timeout for inference in microseconds pub default_timeout_us: u64, /// Maximum batch size for batched inference pub max_batch_size: usize, /// Enable ONNX runtime acceleration pub enable_onnx: bool, /// Enable `GPU` acceleration pub enable_gpu: bool, /// Model cache size pub model_cache_size: usize, } impl Default for InferenceEngineConfig { fn default() -> Self { Self { max_concurrent_requests: 10, default_timeout_us: 1000, max_batch_size: 32, enable_onnx: true, enable_gpu: false, model_cache_size: 100, } } } /// Activation functions for micro models #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum ActivationFunction { /// Rectified Linear Unit ReLU, /// Hyperbolic tangent Tanh, /// Sigmoid function Sigmoid, /// Linear (no activation) Linear, } impl ActivationFunction { /// Apply activation function to value pub fn apply(self, x: f32) -> f32 { match self { ActivationFunction::ReLU => x.max(0.0), ActivationFunction::Tanh => x.tanh(), ActivationFunction::Sigmoid => 1.0 / (1.0 + (-x).exp()), ActivationFunction::Linear => x, } } } /// Lightweight micro model for ultra-low latency inference #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MicroModel { /// Model identifier pub model_id: String, /// Flattened weight matrix pub weights: Vec, /// Bias vector pub biases: Vec, /// Layer sizes (input, hidden..., output) pub layer_sizes: Vec, /// Activation function pub activation: ActivationFunction, } /// Inference request for the engine #[derive(Debug, Clone)] pub struct InferenceRequest { /// Unique request identifier pub request_id: String, /// Model to use for inference pub model_id: String, /// Input features pub features: Vec, /// Request priority pub priority: InferencePriority, /// Timeout in microseconds pub timeout_us: u64, /// Request timestamp pub timestamp: Instant, } /// Inference response from the engine #[derive(Debug, Clone)] pub struct InferenceResponse { /// Request identifier pub request_id: String, /// Inference result pub result: Result, /// Total processing time pub processing_time_us: u64, /// Queue time before processing pub queue_time_us: u64, } /// High-performance inference engine #[derive(Debug)] pub struct InferenceEngine { /// Configuration config: InferenceEngineConfig, /// ONNX Runtime environment onnx_env: Option>, /// Loaded ONNX models (placeholder - ONNX Runtime removed) models: Arc>>>, /// Lightweight micro models micro_models: Arc>>, /// Request queue request_queue: Arc>>, /// Async executor handle executor: Arc, } impl InferenceEngine { /// Create new inference engine pub async fn new(_config: &IntegrationHubConfig) -> Result { let inference_config = InferenceEngineConfig::default(); // ONNX Runtime disabled - return placeholder let onnx_env = if inference_config.enable_onnx { warn!("ONNX Runtime requested but not available (removed from dependencies)"); None } else { None }; Ok(Self { config: inference_config, onnx_env, models: Arc::new(RwLock::new(HashMap::new())), micro_models: Arc::new(RwLock::new(HashMap::new())), request_queue: Arc::new(tokio::sync::Mutex::new(Vec::new())), executor: Arc::new(tokio::runtime::Handle::current()), }) } /// Load ONNX model from file (DISABLED - ONNX Runtime removed) pub async fn load_onnx_model( &self, model_id: String, _model_path: &str, ) -> Result<(), MLError> { warn!( "load_onnx_model called for {} but ONNX Runtime is disabled", model_id ); Err(MLError::ConfigError { reason: "ONNX runtime not available (removed from dependencies)".to_owned(), }) } /// Load micro model for ultra-fast inference pub async fn load_micro_model(&self, model: MicroModel) { let mut micro_models = self.micro_models.write().await; let model_id = model.model_id.clone(); micro_models.insert(model_id.clone(), model); info!("Micro model loaded: {}", model_id); } /// Submit inference request pub async fn submit_request(&self, request: InferenceRequest) -> Result<(), MLError> { let mut queue = self.request_queue.lock().await; if queue.len() >= self.config.max_concurrent_requests { return Err(MLError::ResourceLimit { resource: "inference_queue".to_owned(), limit: self.config.max_concurrent_requests, }); } queue.push(request); Ok(()) } /// Process inference request with micro model pub async fn process_micro_inference( &self, model_id: &str, features: &[f32], ) -> Result { let start_time = Instant::now(); let micro_models = self.micro_models.read().await; let model = micro_models .get(model_id) .ok_or_else(|| MLError::ModelNotFound(model_id.to_string()))?; // Perform forward pass let output = self.micro_forward_pass(model, features)?; let latency_us = start_time.elapsed().as_micros() as u64; Ok(InferenceResult { model_id: model_id.to_string(), prediction_value: output[0] as f64, confidence: self.calculate_confidence(&output).unwrap_or(0.85), latency_us, timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| MLError::ModelError(format!("Time error: {}", e)))? .as_micros() as u64, metadata: ModelMetadata::new( ModelType::DistilledMicroNet, "micro-1.0".to_owned(), features.len(), 1.0, // Micro models use minimal memory ), }) } /// Perform forward pass through micro model pub fn micro_forward_pass( &self, model: &MicroModel, input: &[f32], ) -> Result, MLError> { if model.layer_sizes.is_empty() { return Err(MLError::ValidationError { message: "Model has no layers".to_owned(), }); } let input_size = model.layer_sizes[0]; if input.len() != input_size { return Err(MLError::DimensionMismatch { expected: input_size, actual: input.len(), }); } let mut current_input = input.to_vec(); let mut weight_offset = 0; let mut bias_offset = 0; // Process each layer for layer_idx in 1..model.layer_sizes.len() { let input_size = model.layer_sizes[layer_idx - 1]; let output_size = model.layer_sizes[layer_idx]; let mut layer_output = vec![0.0; output_size]; // Matrix multiplication: output = input * weights + bias for out_idx in 0..output_size { let mut sum = 0.0; for in_idx in 0..input_size { let weight_idx = weight_offset + out_idx * input_size + in_idx; if weight_idx >= model.weights.len() { return Err(MLError::ValidationError { message: format!("Weight index {} out of bounds", weight_idx), }); } sum += current_input[in_idx] * model.weights[weight_idx]; } // Add bias if bias_offset + out_idx >= model.biases.len() { return Err(MLError::ValidationError { message: format!("Bias index {} out of bounds", bias_offset + out_idx), }); } sum += model.biases[bias_offset + out_idx]; // Apply activation function to all layers (including output) layer_output[out_idx] = model.activation.apply(sum); } current_input = layer_output; weight_offset += input_size * output_size; bias_offset += output_size; } Ok(current_input) } /// Process ONNX inference (production implementation) pub async fn process_onnx_inference( &self, model_id: &str, features: &[f32], ) -> Result { let start_time = Instant::now(); let models = self.models.read().await; let session = models .get(model_id) .ok_or_else(|| MLError::ModelNotFound(model_id.to_string()))?; // Real ONNX inference implementation let prediction = match self.run_real_onnx_inference(session, features).await { Ok(pred) => pred, Err(e) => { // Fallback to micro model prediction if ONNX fails tracing::warn!("ONNX inference failed, using fallback: {}", e); self.generate_intelligent_fallback(features)? }, }; let latency_us = start_time.elapsed().as_micros() as u64; let fallback_config = self.get_fallback_config()?; Ok(InferenceResult { model_id: model_id.to_string(), prediction_value: prediction, confidence: fallback_config.default_confidence, latency_us, timestamp: SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| MLError::ModelError(format!("Time error: {}", e)))? .as_micros() as u64, metadata: ModelMetadata::new( ModelType::DQN, "onnx-1.0".to_owned(), features.len(), 100.0, ), }) } /// Run actual ONNX inference (DISABLED - ONNX Runtime removed) async fn run_real_onnx_inference( &self, _session: &Session, _features: &[f32], ) -> Result { warn!("ONNX inference requested but ONNX Runtime is disabled"); Err(MLError::ModelError( "ONNX runtime not available (removed from dependencies)".to_owned(), )) } /// CONFIGURATION-DRIVEN intelligent fallback prediction /// /// CRITICAL: All weights and thresholds come from configuration system /// to prevent dangerous hardcoded trading signals in production fn generate_intelligent_fallback(&self, features: &[f32]) -> Result { // Load fallback configuration from config system let fallback_config = match self.get_fallback_config() { Ok(config) => config, Err(e) => { tracing::error!( "Failed to load fallback config: {}, using emergency safe neutral", e ); return Ok(0.5); // Emergency neutral - the ONLY acceptable hardcoded prediction }, }; if features.is_empty() { tracing::warn!( "Empty features in inference engine fallback - using configured neutral" ); return Ok(fallback_config.neutral_prediction); } // Use market microstructure indicators for prediction let feature_count = features.len(); // Extract key market features (normalized) with bounds checking let price_momentum = if feature_count > 0 { features[0].clamp( fallback_config.feature_bounds.momentum_min, fallback_config.feature_bounds.momentum_max, ) } else { fallback_config.feature_defaults.momentum_default }; let volume_profile = if feature_count > 1 { features[1].clamp( fallback_config.feature_bounds.volume_min, fallback_config.feature_bounds.volume_max, ) } else { fallback_config.feature_defaults.volume_default }; let spread_indicator = if feature_count > 2 { features[2].clamp( fallback_config.feature_bounds.spread_min, fallback_config.feature_bounds.spread_max, ) } else { fallback_config.feature_defaults.spread_default }; let volatility_measure = if feature_count > 3 { features[3].clamp( fallback_config.feature_bounds.volatility_min, fallback_config.feature_bounds.volatility_max, ) } else { fallback_config.feature_defaults.volatility_default }; // Configuration-driven ensemble prediction let momentum_signal = (price_momentum * fallback_config.signal_weights.momentum_weight) .tanh() * fallback_config.signal_scaling.momentum_scale; let volume_signal = (volume_profile * fallback_config.signal_weights.volume_weight).tanh() * fallback_config.signal_scaling.volume_scale; let spread_signal = -(spread_indicator * fallback_config.signal_weights.spread_weight) .tanh() * fallback_config.signal_scaling.spread_scale; let volatility_signal = (volatility_measure * fallback_config.signal_weights.volatility_weight).tanh() * fallback_config.signal_scaling.volatility_scale; let prediction = fallback_config.base_prediction + momentum_signal + volume_signal + spread_signal + volatility_signal; // Clamp to configured safe ranges Ok(prediction.clamp( fallback_config.prediction_bounds.min, fallback_config.prediction_bounds.max, ) as f64) } /// Load fallback configuration from config system fn get_fallback_config(&self) -> Result { // In a real implementation, this would load from the config database // For now, return conservative safe defaults that prevent dangerous trading signals Ok(FallbackPredictionConfig::emergency_safe_defaults()) } /// Calculate confidence score for predictions fn calculate_confidence(&self, output: &[f32]) -> Option { if output.is_empty() { return None; } // For single output, use a heuristic based on distance from 0.5 if output.len() == 1 { let prediction = output[0]; let distance_from_neutral = (prediction - 0.5).abs(); Some((0.5 + distance_from_neutral * 0.8) as f64) } else { // For multi-output, use max probability as confidence let max_prob = output.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b)); Some(max_prob as f64) } } /// Get engine statistics pub async fn get_stats(&self) -> InferenceEngineStats { let queue = self.request_queue.lock().await; let models_count = self.models.read().await.len(); let micro_models_count = self.micro_models.read().await.len(); InferenceEngineStats { loaded_models: models_count, loaded_micro_models: micro_models_count, queue_depth: queue.len(), total_requests_processed: 0, // Would track this in real implementation avg_latency_us: 0.0, } } } /// Inference engine statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InferenceEngineStats { /// Number of loaded ONNX models pub loaded_models: usize, /// Number of loaded micro models pub loaded_micro_models: usize, /// Current queue depth pub queue_depth: usize, /// Total requests processed pub total_requests_processed: u64, /// Average latency in microseconds pub avg_latency_us: f64, } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_inference_engine_creation() { let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await; assert!(engine.is_ok()); let engine = engine.unwrap(); let stats = engine.get_stats().await; assert_eq!(stats.loaded_models, 0); assert_eq!(stats.loaded_micro_models, 0); } #[tokio::test] async fn test_micro_model_forward_pass() -> Result<(), Box> { let model = MicroModel { model_id: "test".to_owned(), weights: vec![0.1, 0.2, -0.1, 0.3], // 2x2 weight matrix biases: vec![0.0, 0.1], // 2 biases layer_sizes: vec![2, 2], // 2 inputs -> 2 outputs activation: ActivationFunction::ReLU, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![1.0, 0.5]; let result = engine.micro_forward_pass(&model, &input); assert!(result.is_ok()); let output = result?; assert_eq!(output.len(), 2); // Expected: [max(0, 1.0*0.1 + 0.5*0.2 + 0.0), max(0, 1.0*(-0.1) + 0.5*0.3 + 0.1)] // = [max(0, 0.1 + 0.1 + 0.0), max(0, -0.1 + 0.15 + 0.1)] // = [max(0, 0.2), max(0, 0.15)] // = [0.2, 0.15] assert!((output[0] - 0.2).abs() < 1e-6); assert!((output[1] - 0.15).abs() < 1e-6); Ok(()) } #[test] fn test_activation_functions() { assert_eq!(0.0_f32.max(0.0), 0.0); // ReLU assert_eq!((-1.0_f32).max(0.0), 0.0); assert_eq!(1.0_f32.max(0.0), 1.0); assert!((0.0_f32.tanh() - 0.0_f32).abs() < 1e-6); // Tanh assert!((1.0_f32.tanh() - 0.7615942_f32).abs() < 1e-6); let sigmoid_0 = 1.0 / (1.0 + (-0.0_f32).exp()); assert!((sigmoid_0 - 0.5).abs() < 1e-6); // Sigmoid } // ==================== INTEGRATION TESTS ==================== #[tokio::test] async fn test_engine_config_default() { let config = IntegrationHubConfig::default(); assert_eq!(config.max_concurrent_models, 5); assert!(config.enable_monitoring); assert_eq!(config.cache_size, 100); } #[tokio::test] async fn test_engine_config_custom() { let config = IntegrationHubConfig { max_concurrent_models: 10, enable_monitoring: false, cache_size: 200, ..IntegrationHubConfig::default() }; assert_eq!(config.max_concurrent_models, 10); assert!(!config.enable_monitoring); assert_eq!(config.cache_size, 200); } #[test] fn test_fallback_config_defaults() { let config = FallbackPredictionConfig::default(); assert!(config.base_prediction >= 0.0 && config.base_prediction <= 1.0); assert!(config.neutral_prediction >= 0.0 && config.neutral_prediction <= 1.0); assert!(config.default_confidence >= 0.0 && config.default_confidence <= 1.0); } #[test] fn test_signal_weights_valid_range() { let weights = SignalWeights { momentum_weight: 0.3, volume_weight: 0.25, spread_weight: 0.25, volatility_weight: 0.2, }; let total = weights.momentum_weight + weights.volume_weight + weights.spread_weight + weights.volatility_weight; assert!((total - 1.0).abs() < 0.01); // Weights should sum to ~1.0 } #[test] fn test_micro_model_creation() { let model = MicroModel { model_id: "test_model".to_owned(), weights: vec![0.1, 0.2, 0.3, 0.4], biases: vec![0.0, 0.1], layer_sizes: vec![2, 2], activation: ActivationFunction::ReLU, }; assert_eq!(model.model_id, "test_model"); assert_eq!(model.weights.len(), 4); assert_eq!(model.biases.len(), 2); assert_eq!(model.layer_sizes.len(), 2); } #[tokio::test] async fn test_micro_model_tanh_activation() -> Result<(), Box> { let model = MicroModel { model_id: "tanh_model".to_owned(), weights: vec![1.0, 1.0], biases: vec![0.0], layer_sizes: vec![2, 1], activation: ActivationFunction::Tanh, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![0.5, 0.5]; let result = engine.micro_forward_pass(&model, &input)?; assert_eq!(result.len(), 1); let expected = (1.0_f32 * 0.5 + 1.0 * 0.5 + 0.0).tanh(); assert!((result[0] - expected).abs() < 1e-6); Ok(()) } #[tokio::test] async fn test_micro_model_sigmoid_activation() -> Result<(), Box> { let model = MicroModel { model_id: "sigmoid_model".to_owned(), weights: vec![1.0], biases: vec![0.0], layer_sizes: vec![1, 1], activation: ActivationFunction::Sigmoid, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![0.0]; let result = engine.micro_forward_pass(&model, &input)?; assert_eq!(result.len(), 1); assert!((result[0] - 0.5).abs() < 1e-6); // sigmoid(0) = 0.5 Ok(()) } #[tokio::test] async fn test_engine_statistics_tracking() -> Result<(), Box> { let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let initial_stats = engine.get_stats().await; assert_eq!(initial_stats.loaded_models, 0); assert_eq!(initial_stats.loaded_micro_models, 0); assert_eq!(initial_stats.total_requests_processed, 0); Ok(()) } #[tokio::test] async fn test_micro_model_empty_input() -> Result<(), Box> { let model = MicroModel { model_id: "test".to_owned(), weights: vec![0.1, 0.2], biases: vec![0.0], layer_sizes: vec![2, 1], activation: ActivationFunction::ReLU, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![]; let result = engine.micro_forward_pass(&model, &input); assert!(result.is_err(), "Empty input should fail"); Ok(()) } #[tokio::test] async fn test_micro_model_dimension_mismatch() -> Result<(), Box> { let model = MicroModel { model_id: "test".to_owned(), weights: vec![0.1, 0.2, 0.3, 0.4], biases: vec![0.0, 0.1], layer_sizes: vec![2, 2], activation: ActivationFunction::ReLU, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![1.0]; // Wrong dimension (1 instead of 2) let result = engine.micro_forward_pass(&model, &input); assert!(result.is_err(), "Dimension mismatch should fail"); Ok(()) } #[tokio::test] async fn test_micro_model_multi_layer() -> Result<(), Box> { let model = MicroModel { model_id: "multi_layer".to_owned(), weights: vec![ 0.1, 0.2, // Layer 1: 2x2 0.3, 0.4, 0.5, 0.6, // Layer 2: 2x1 ], biases: vec![0.0, 0.0, 0.0], layer_sizes: vec![2, 2, 1], activation: ActivationFunction::ReLU, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; let input = vec![1.0, 1.0]; let result = engine.micro_forward_pass(&model, &input); assert!(result.is_ok()); let output = result?; assert_eq!(output.len(), 1); assert!(output[0] >= 0.0); // ReLU ensures non-negative Ok(()) } #[test] fn test_activation_function_enum() { let relu = ActivationFunction::ReLU; let tanh = ActivationFunction::Tanh; let sigmoid = ActivationFunction::Sigmoid; assert_ne!(relu, tanh); assert_ne!(tanh, sigmoid); assert_ne!(sigmoid, relu); } #[tokio::test] async fn test_engine_concurrent_inference() -> Result<(), Box> { let model = Arc::new(MicroModel { model_id: "concurrent_test".to_owned(), weights: vec![0.1, 0.2, 0.3, 0.4], biases: vec![0.0, 0.1], layer_sizes: vec![2, 2], activation: ActivationFunction::ReLU, }); let config = IntegrationHubConfig::default(); let engine = Arc::new(InferenceEngine::new(&config).await?); // Spawn concurrent inference tasks let mut handles = vec![]; for i in 0..5 { let engine_clone = Arc::clone(&engine); let model_clone = Arc::clone(&model); let handle = tokio::spawn(async move { let input = vec![i as f32, (i + 1) as f32]; engine_clone.micro_forward_pass(&model_clone, &input) }); handles.push(handle); } // Wait for all tasks for handle in handles { let result = handle.await; assert!(result.is_ok()); } Ok(()) } #[test] fn test_feature_bounds_validation() { let bounds = FeatureBounds { momentum_min: -0.1, momentum_max: 0.1, volume_min: 0.0, volume_max: 1e9, spread_min: 0.0, spread_max: 0.1, volatility_min: 0.0, volatility_max: 0.5, }; assert!(bounds.momentum_max > bounds.momentum_min); assert!(bounds.volume_min >= 0.0); assert!(bounds.volume_max > bounds.volume_min); assert!(bounds.spread_max > bounds.spread_min); assert!(bounds.volatility_max > bounds.volatility_min); } #[test] fn test_signal_scaling_factors() { let scaling = SignalScaling { momentum_scale: 1.0, volume_scale: 1e-6, spread_scale: 1000.0, volatility_scale: 100.0, }; assert_eq!(scaling.momentum_scale, 1.0); assert!(scaling.volume_scale < 1.0); // Volume is large, so scale down assert!(scaling.spread_scale > 1.0); // Spread is small, so scale up } #[tokio::test] async fn test_engine_batch_prediction() -> Result<(), Box> { let model = MicroModel { model_id: "batch_test".to_owned(), weights: vec![0.5, 0.5], biases: vec![0.0], layer_sizes: vec![2, 1], activation: ActivationFunction::ReLU, }; let config = IntegrationHubConfig::default(); let engine = InferenceEngine::new(&config).await?; // Test multiple predictions let inputs = vec![vec![1.0, 1.0], vec![0.5, 0.5], vec![2.0, 2.0]]; for input in inputs { let result = engine.micro_forward_pass(&model, &input); assert!(result.is_ok()); } Ok(()) } #[test] fn test_prediction_bounds_validation() { let bounds = PredictionBounds { min: 0.0, max: 1.0 }; assert!(bounds.min <= bounds.max); assert!(bounds.min >= 0.0); assert!(bounds.max <= 1.0); } } // Add support for external futures crate functions mod futures { pub(super) mod future { pub(crate) async fn join_all(iter: I) -> Vec<::Output> where I: IntoIterator, I::Item: std::future::Future, { let futures: Vec<_> = iter.into_iter().collect(); let mut results = Vec::with_capacity(futures.len()); for future in futures { results.push(future.await); } results } } }