/// Builder for ConfigManager with advanced configuration options. /// /// Provides a fluent interface for constructing ConfigManager instances /// with optional asset classification, caching, and database integration. pub struct ConfigManagerBuilder { config: ServiceConfig, asset_manager: Option, cache_timeout: std::time::Duration, } impl ConfigManagerBuilder { /// Creates a new ConfigManagerBuilder with the specified service configuration. pub const fn new(config: ServiceConfig) -> Self { Self { config, asset_manager: None, cache_timeout: std::time::Duration::from_secs(300), } } /// Sets the asset classification manager. pub fn with_asset_classification( mut self, manager: crate::asset_classification::AssetClassificationManager, ) -> Self { self.asset_manager = Some(manager); self } /// Sets the cache timeout duration. pub const fn with_cache_timeout(mut self, timeout: std::time::Duration) -> Self { self.cache_timeout = timeout; self } /// Builds the ConfigManager with the specified configuration. pub fn build(self) -> ConfigManager { ConfigManager { config: Arc::new(self.config), asset_classification: Arc::new(RwLock::new(self.asset_manager)), cache: Arc::new(RwLock::new(HashMap::new())), cache_timeout: self.cache_timeout, } } /// Builds the ConfigManager with database integration. /// /// # Errors /// Returns error if the operation fails #[cfg(feature = "postgres")] pub async fn build_with_database( self, database_pool: sqlx::PgPool, ) -> Result> { let manager = self.build(); manager .initialize_asset_classification(database_pool) .await?; Ok(manager) } } // Configuration management and service configuration structures. // // This module provides the core configuration management infrastructure for // the Foxhunt trading system. It handles service-specific configuration, // environment management, and provides thread-safe access to configuration // data across the application. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; /// Service-specific configuration structure. /// /// Contains metadata and settings for a specific service in the Foxhunt /// trading system. Supports environment-specific configuration and /// versioning for configuration management and deployment tracking. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceConfig { /// Service name (e.g., "trading_service", "ml_training_service") pub name: String, /// Deployment environment (e.g., "development", "staging", "production") pub environment: String, /// Service version for deployment tracking pub version: String, /// Service-specific configuration settings as JSON pub settings: serde_json::Value, } /// Thread-safe configuration manager for comprehensive service configuration. /// /// Provides centralized access to service configuration with support for: /// - Asset classification management /// /// - Hot-reload capabilities /// - Environment-specific settings /// /// - Thread-safe access patterns /// /// Ensures configuration consistency across all components of a service. pub struct ConfigManager { config: Arc, /// Asset classification manager for symbol-based configuration asset_classification: Arc>>, /// Configuration cache for performance cache: Arc)>>>, /// Cache timeout duration cache_timeout: std::time::Duration, } impl ConfigManager { /// Creates a new ConfigManager with the provided service configuration. /// /// The configuration is wrapped in an Arc for efficient sharing across /// multiple threads and components within the service. /// /// # Arguments /// /// * `config` - The service configuration to manage pub fn new(config: ServiceConfig) -> Self { Self { config: Arc::new(config), asset_classification: Arc::new(RwLock::new(None)), cache: Arc::new(RwLock::new(HashMap::new())), cache_timeout: std::time::Duration::from_secs(300), // 5 minutes } } /// Creates a new ConfigManager with asset classification support. /// /// Initializes the manager with both service configuration and /// asset classification capabilities for comprehensive trading /// parameter management. /// /// # Arguments /// /// * `config` - The service configuration to manage /// * `asset_manager` - Pre-configured asset classification manager pub fn with_asset_classification( config: ServiceConfig, asset_manager: crate::asset_classification::AssetClassificationManager, ) -> Self { Self { config: Arc::new(config), asset_classification: Arc::new(RwLock::new(Some(asset_manager))), cache: Arc::new(RwLock::new(HashMap::new())), cache_timeout: std::time::Duration::from_secs(300), } } /// Returns a shared reference to the service configuration. /// /// Provides thread-safe access to the configuration data through Arc cloning. /// /// The returned Arc can be shared across threads without additional locking. /// /// # Returns /// /// An Arc containing the service configuration pub fn get_config(&self) -> Arc { Arc::clone(&self.config) } /// Initializes asset classification with database-backed configurations. /// /// Loads asset classification configurations from the database and /// initializes the asset classification manager for dynamic symbol /// classification and trading parameter retrieval. /// /// # Errors /// Returns error if the operation fails #[cfg(feature = "postgres")] pub async fn initialize_asset_classification( &self, database_pool: sqlx::PgPool, ) -> Result<(), Box> { let mut loader = crate::database::PostgresAssetClassificationLoader::with_pool(database_pool); let configs = loader.load_asset_configurations().await?; let mut manager = crate::asset_classification::AssetClassificationManager::new(); manager.load_configurations(configs).await?; if let Ok(mut asset_classification) = self.asset_classification.write() { *asset_classification = Some(manager); } Ok(()) } /// Classifies a symbol using the asset classification manager. /// /// Returns the asset class for the given symbol based on configured /// pattern matching rules and explicit mappings. /// /// # Arguments /// /// * `symbol` - The trading symbol to classify /// /// # Returns /// /// The asset class or Unknown if classification fails pub fn classify_symbol(&self, symbol: &str) -> crate::asset_classification::AssetClass { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.classify_symbol(symbol); } } crate::asset_classification::AssetClass::Unknown } /// Gets trading parameters for a symbol. /// /// Retrieves comprehensive trading parameters including position limits, /// risk thresholds, and execution configuration for the specified symbol. /// /// # Arguments /// /// * `symbol` - The trading symbol /// /// # Returns /// /// Trading parameters if available, None otherwise pub fn get_trading_parameters( &self, symbol: &str, ) -> Option { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.get_trading_parameters(symbol).cloned(); } } None } /// Gets volatility profile for a symbol. /// /// Retrieves the volatility profile including base volatility, /// stress multipliers, and jump risk characteristics. /// /// # Arguments /// /// * `symbol` - The trading symbol /// /// # Returns /// /// Volatility profile if available, None otherwise pub fn get_volatility_profile( &self, symbol: &str, ) -> Option { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.get_volatility_profile(symbol).cloned(); } } None } /// Gets daily volatility estimate for a symbol. /// /// Calculates the daily volatility from the annual volatility /// using standard financial mathematics (annual / sqrt(252)). /// /// # Arguments /// /// * `symbol` - The trading symbol /// /// # Returns /// /// Daily volatility estimate as a decimal pub fn get_daily_volatility(&self, symbol: &str) -> f64 { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.get_daily_volatility(symbol); } } 0.05 // Default 5% daily volatility for unknown symbols } /// Gets position size recommendation for a symbol. /// /// Calculates recommended position size based on portfolio NAV /// and the symbol's configured position limits. /// /// # Arguments /// /// * `symbol` - The trading symbol /// * `portfolio_nav` - Current portfolio net asset value /// /// # Returns /// /// Recommended position size if available pub fn get_position_size_recommendation( &self, symbol: &str, portfolio_nav: rust_decimal::Decimal, ) -> Option { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.get_position_size_recommendation(symbol, portfolio_nav); } } None } /// Checks if trading is active for a symbol at the given time. /// /// Validates trading hours and market schedule for the symbol. /// /// # Arguments /// /// * `symbol` - The trading symbol /// * `timestamp` - The timestamp to check /// /// # Returns /// /// True if trading is active, false otherwise pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime) -> bool { if let Ok(asset_classification) = self.asset_classification.read() { if let Some(ref manager) = *asset_classification { return manager.is_trading_active(symbol, timestamp); } } true // Default to always active if no classification available } /// Reloads asset classification configurations. /// /// Triggers a reload of asset classification configurations /// for hot-reload functionality in production environments. /// /// # Errors /// Returns error if the operation fails #[cfg(feature = "postgres")] pub async fn reload_asset_classification( &self, database_pool: sqlx::PgPool, ) -> Result<(), Box> { let needs_reload = { let asset_classification = self.asset_classification.read().ok(); asset_classification .as_ref() .and_then(|ac| ac.as_ref()) .map(|manager| manager.needs_reload()) .unwrap_or(false) }; // Lock released here if needs_reload { self.initialize_asset_classification(database_pool).await?; } Ok(()) } /// Gets cached configuration value. /// /// Retrieves a cached configuration value with automatic expiration. /// /// # Arguments /// /// * `key` - Cache key /// /// # Returns /// /// Cached value if available and not expired pub fn get_cached_config(&self, key: &str) -> Option { if let Ok(cache) = self.cache.read() { if let Some((value, timestamp)) = cache.get(key) { let elapsed = Utc::now().signed_duration_since(*timestamp); if elapsed.to_std().unwrap_or_default() < self.cache_timeout { return Some(value.clone()); } } } None } /// Sets cached configuration value. /// /// Stores a configuration value in the cache with timestamp. /// /// # Arguments /// /// * `key` - Cache key /// * `value` - Value to cache pub fn set_cached_config(&self, key: String, value: serde_json::Value) { if let Ok(mut cache) = self.cache.write() { cache.insert(key, (value, Utc::now())); } } /// Clears expired cache entries. /// /// Removes cache entries that have exceeded the timeout duration. pub fn cleanup_cache(&self) { if let Ok(mut cache) = self.cache.write() { let now = Utc::now(); cache.retain(|_, (_, timestamp)| { let elapsed = now.signed_duration_since(*timestamp); elapsed.to_std().unwrap_or_default() < self.cache_timeout }); } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn create_test_config() -> ServiceConfig { ServiceConfig { name: "test_service".to_owned(), environment: "test".to_owned(), version: "1.0.0".to_owned(), settings: json!({"test_key": "test_value"}), } } #[test] fn test_service_config_creation() { let config = create_test_config(); assert_eq!(config.name, "test_service"); assert_eq!(config.environment, "test"); assert_eq!(config.version, "1.0.0"); } #[test] fn test_config_manager_new() { let config = create_test_config(); let manager = ConfigManager::new(config); let retrieved_config = manager.get_config(); assert_eq!(retrieved_config.name, "test_service"); } #[test] fn test_config_manager_builder() { let config = create_test_config(); let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(std::time::Duration::from_secs(60)) .build(); let retrieved_config = manager.get_config(); assert_eq!(retrieved_config.name, "test_service"); } #[test] fn test_config_manager_cache_set_and_get() { let config = create_test_config(); let manager = ConfigManager::new(config); let test_value = json!({"cached": "data"}); manager.set_cached_config("test_key".to_owned(), test_value.clone()); let retrieved = manager.get_cached_config("test_key"); assert!(retrieved.is_some()); assert_eq!(retrieved.unwrap(), test_value); } #[test] fn test_config_manager_cache_miss() { let config = create_test_config(); let manager = ConfigManager::new(config); let retrieved = manager.get_cached_config("nonexistent_key"); assert!(retrieved.is_none()); } #[test] fn test_config_manager_cleanup_cache() { let config = create_test_config(); let manager = ConfigManager::new(config); let test_value = json!({"cached": "data"}); manager.set_cached_config("test_key".to_owned(), test_value); manager.cleanup_cache(); // Cache entry should still exist since it was just created let retrieved = manager.get_cached_config("test_key"); assert!(retrieved.is_some()); } #[test] fn test_config_manager_classify_symbol_without_asset_manager() { let config = create_test_config(); let manager = ConfigManager::new(config); let asset_class = manager.classify_symbol("AAPL"); assert_eq!( asset_class, crate::asset_classification::AssetClass::Unknown ); } #[test] fn test_config_manager_get_daily_volatility_default() { let config = create_test_config(); let manager = ConfigManager::new(config); let volatility = manager.get_daily_volatility("AAPL"); assert_eq!(volatility, 0.05); // Default value } #[test] fn test_config_manager_is_trading_active_default() { let config = create_test_config(); let manager = ConfigManager::new(config); let now = chrono::Utc::now(); let is_active = manager.is_trading_active("AAPL", now); assert!(is_active); // Default to always active } #[test] fn test_config_manager_get_trading_parameters_none() { let config = create_test_config(); let manager = ConfigManager::new(config); let params = manager.get_trading_parameters("AAPL"); assert!(params.is_none()); } #[test] fn test_config_manager_get_volatility_profile_none() { let config = create_test_config(); let manager = ConfigManager::new(config); let profile = manager.get_volatility_profile("AAPL"); assert!(profile.is_none()); } #[test] fn test_config_manager_get_position_size_recommendation_none() { let config = create_test_config(); let manager = ConfigManager::new(config); let recommendation = manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0)); assert!(recommendation.is_none()); } #[test] fn test_config_manager_with_asset_classification() { let config = create_test_config(); let asset_manager = crate::asset_classification::AssetClassificationManager::new(); let manager = ConfigManager::with_asset_classification(config, asset_manager); let retrieved_config = manager.get_config(); assert_eq!(retrieved_config.name, "test_service"); } #[test] fn test_builder_with_asset_classification() { let config = create_test_config(); let asset_manager = crate::asset_classification::AssetClassificationManager::new(); let manager = ConfigManagerBuilder::new(config) .with_asset_classification(asset_manager) .build(); let retrieved_config = manager.get_config(); assert_eq!(retrieved_config.name, "test_service"); } #[test] fn test_service_config_serialization() { let config = create_test_config(); let serialized = serde_json::to_string(&config).unwrap(); let deserialized: ServiceConfig = serde_json::from_str(&serialized).unwrap(); assert_eq!(config.name, deserialized.name); assert_eq!(config.environment, deserialized.environment); assert_eq!(config.version, deserialized.version); } #[test] fn test_config_manager_multiple_cache_entries() { let config = create_test_config(); let manager = ConfigManager::new(config); for i in 0..10 { manager.set_cached_config(format!("key_{}", i), json!({"value": i})); } for i in 0..10 { let retrieved = manager.get_cached_config(&format!("key_{}", i)); assert!(retrieved.is_some()); } } #[test] fn test_config_manager_cache_overwrite() { let config = create_test_config(); let manager = ConfigManager::new(config); manager.set_cached_config("key".to_owned(), json!({"value": 1})); manager.set_cached_config("key".to_owned(), json!({"value": 2})); let retrieved = manager.get_cached_config("key"); assert_eq!(retrieved.unwrap(), json!({"value": 2})); } #[test] fn test_builder_custom_cache_timeout() { let config = create_test_config(); let custom_timeout = std::time::Duration::from_secs(120); let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(custom_timeout) .build(); // Cache timeout is set internally let retrieved_config = manager.get_config(); assert_eq!(retrieved_config.name, "test_service"); } #[test] fn test_config_manager_shared_config() { let config = create_test_config(); let manager = ConfigManager::new(config); let config1 = manager.get_config(); let config2 = manager.get_config(); // Both should point to the same Arc assert_eq!(config1.name, config2.name); } #[test] fn test_service_config_clone() { let config1 = create_test_config(); let config2 = config1.clone(); assert_eq!(config1.name, config2.name); assert_eq!(config1.environment, config2.environment); assert_eq!(config1.version, config2.version); } #[test] fn test_config_manager_cache_timeout_configuration() { let config = create_test_config(); let custom_timeout = std::time::Duration::from_millis(10); let manager = ConfigManagerBuilder::new(config) .with_cache_timeout(custom_timeout) .build(); // Cache timeout is configured internally assert_eq!(manager.cache_timeout, custom_timeout); // Test that cache still works normally manager.set_cached_config("test_key".to_owned(), json!({"value": 42})); assert!(manager.get_cached_config("test_key").is_some()); } #[test] fn test_config_manager_concurrent_access() { use std::sync::Arc; use std::thread; let config = create_test_config(); let manager = Arc::new(ConfigManager::new(config)); let mut handles = vec![]; for i in 0..10 { let manager_clone = Arc::clone(&manager); let handle = thread::spawn(move || { manager_clone .set_cached_config(format!("concurrent_key_{}", i), json!({"thread_id": i})); manager_clone.get_cached_config(&format!("concurrent_key_{}", i)) }); handles.push(handle); } for handle in handles { assert!(handle.join().unwrap().is_some()); } } #[test] fn test_config_manager_daily_volatility_fallback() { let config = create_test_config(); let manager = ConfigManager::new(config); // Should return default 5% for unknown symbols let vol = manager.get_daily_volatility("UNKNOWN_SYMBOL"); assert_eq!(vol, 0.05); } #[test] fn test_config_manager_position_size_none() { let config = create_test_config(); let manager = ConfigManager::new(config); // Should return None without asset classification let size = manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0)); assert!(size.is_none()); } #[test] fn test_service_config_validation() { let mut config = create_test_config(); // Valid config assert!(!config.name.is_empty()); assert!(!config.environment.is_empty()); // Test with empty name config.name = String::new(); assert!(config.name.is_empty()); } #[test] fn test_config_manager_cache_clear() { let config = create_test_config(); let manager = ConfigManager::new(config); // Add some cache entries manager.set_cached_config("key1".to_owned(), json!({"value": 1})); manager.set_cached_config("key2".to_owned(), json!({"value": 2})); assert!(manager.get_cached_config("key1").is_some()); assert!(manager.get_cached_config("key2").is_some()); // Manual clear if let Ok(mut cache) = manager.cache.write() { cache.clear(); } assert!(manager.get_cached_config("key1").is_none()); assert!(manager.get_cached_config("key2").is_none()); } #[test] fn test_builder_default_values() { let config = create_test_config(); let manager = ConfigManagerBuilder::new(config.clone()).build(); let retrieved = manager.get_config(); assert_eq!(retrieved.name, config.name); assert_eq!(retrieved.environment, config.environment); } #[test] fn test_config_manager_arc_cloning() { let config = create_test_config(); let manager = ConfigManager::new(config); let config1 = Arc::clone(&manager.config); let config2 = Arc::clone(&manager.config); assert_eq!(config1.name, config2.name); assert_eq!(Arc::strong_count(&manager.config), 3); // Original + 2 clones } }