Eliminate ~4,260 clippy deny-level errors that blocked workspace-wide clippy runs. Errors cascaded: upstream crate failures (ctrader-openapi, risk-data) hid thousands of downstream errors in ml, tli, backtesting. Key changes: - ctrader-openapi: fix shadow_unrelated/shadow_reuse (renamed vars) - risk-data/risk: replace non-ASCII em dashes with ASCII equivalents - tli: allow deny lints on prost-generated proto code, fix shadows - trading_engine: fix let_underscore_must_use, wildcard matches, shadows - broker_gateway_service: allow dead_code on unused redis_client field - ml (4030 errors): remove local deny overrides for unwrap/expect/indexing (workspace warn level sufficient), add crate-level allows for non-safety mass-violation lints (non_ascii_literal, shadow_*, str_to_string, etc.), batch-fix em dashes, unseparated literal suffixes, format_push_string, wildcard matches, impl_trait_in_params, mutex_atomic, and more - backtesting: replace unwrap() on first()/last() with match destructure - tests: simplify loop-that-never-loops, fix mutex unwrap Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
991 lines
32 KiB
Rust
991 lines
32 KiB
Rust
//! # 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<f32>,
|
|
/// Bias vector
|
|
pub biases: Vec<f32>,
|
|
/// Layer sizes (input, hidden..., output)
|
|
pub layer_sizes: Vec<usize>,
|
|
/// 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<f32>,
|
|
/// 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<InferenceResult, MLError>,
|
|
/// 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<Arc<Environment>>,
|
|
/// Loaded ONNX models (placeholder - ONNX Runtime removed)
|
|
models: Arc<RwLock<HashMap<String, Arc<Session>>>>,
|
|
/// Lightweight micro models
|
|
micro_models: Arc<RwLock<HashMap<String, MicroModel>>>,
|
|
/// Request queue
|
|
request_queue: Arc<tokio::sync::Mutex<Vec<InferenceRequest>>>,
|
|
/// Async executor handle
|
|
executor: Arc<tokio::runtime::Handle>,
|
|
}
|
|
|
|
impl InferenceEngine {
|
|
/// Create new inference engine
|
|
pub async fn new(_config: &IntegrationHubConfig) -> Result<Self, MLError> {
|
|
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<InferenceResult, MLError> {
|
|
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<Vec<f32>, 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<InferenceResult, MLError> {
|
|
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<f64, MLError> {
|
|
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<f64, MLError> {
|
|
// 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<FallbackPredictionConfig, MLError> {
|
|
// 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<f64> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<I>(iter: I) -> Vec<<I::Item as std::future::Future>::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
|
|
}
|
|
}
|
|
}
|