Files
foxhunt/crates/config/src/manager.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

760 lines
25 KiB
Rust

/// 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<crate::asset_classification::AssetClassificationManager>,
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<ConfigManager, Box<dyn std::error::Error + Send + Sync>> {
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<ServiceConfig>,
/// Asset classification manager for symbol-based configuration
asset_classification:
Arc<RwLock<Option<crate::asset_classification::AssetClassificationManager>>>,
/// Configuration cache for performance
cache: Arc<RwLock<HashMap<String, (serde_json::Value, DateTime<Utc>)>>>,
/// 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<ServiceConfig> {
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<dyn std::error::Error + Send + Sync>> {
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<crate::asset_classification::TradingParameters> {
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<crate::asset_classification::VolatilityProfile> {
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<rust_decimal::Decimal> {
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<Utc>) -> 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<dyn std::error::Error + Send + Sync>> {
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<serde_json::Value> {
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
}
}