//! Database-backed configuration loader for adaptive strategy //! //! This module provides functionality to load adaptive strategy configurations //! from PostgreSQL, replacing hardcoded defaults with database-driven config. //! //! Features: //! - Full configuration loading from database //! - Hot-reload support via PostgreSQL NOTIFY/LISTEN //! - Fallback to default configuration on database errors //! - Validation of loaded configurations #[cfg(feature = "postgres")] use sqlx::postgres::PgListener; #[cfg(feature = "postgres")] use crate::config_types::{AdaptiveStrategyConfig, AdaptiveStrategyConfigRow, ModelConfigRow, FeatureConfigRow}; #[cfg(feature = "postgres")] use std::time::Duration; /// Database-backed configuration loader /// /// Loads adaptive strategy configurations from PostgreSQL and provides /// hot-reload capabilities through NOTIFY/LISTEN. #[cfg(feature = "postgres")] pub struct DatabaseConfigLoader { /// Database connection pool pool: sqlx::PgPool, /// PostgreSQL listener for hot-reload listener: Option, /// Cache timeout for loaded configurations (reserved for future caching implementation) #[allow(dead_code)] cache_timeout: Duration, } #[cfg(feature = "postgres")] impl DatabaseConfigLoader { /// Create a new database configuration loader /// /// # Arguments /// * `database_url` - PostgreSQL connection URL /// /// # Example /// ```no_run /// # use adaptive_strategy::database_loader::DatabaseConfigLoader; /// # async fn example() -> Result<(), Box> { /// let loader = DatabaseConfigLoader::new("postgresql://localhost/foxhunt").await?; /// # Ok(()) /// # } /// ``` pub async fn new(database_url: &str) -> Result { let pool = sqlx::PgPool::connect(database_url).await?; Ok(Self { pool, listener: None, cache_timeout: Duration::from_secs(300), // 5 minutes }) } /// Create a loader with an existing connection pool /// /// # Arguments /// * `pool` - Existing PostgreSQL connection pool pub fn with_pool(pool: sqlx::PgPool) -> Self { Self { pool, listener: None, cache_timeout: Duration::from_secs(300), } } /// Load configuration from database by strategy ID /// /// # Arguments /// * `strategy_id` - Strategy identifier (e.g., "default", "prod_v1") /// /// # Returns /// - `Ok(Some(config))` - Configuration loaded successfully /// - `Ok(None)` - Strategy not found in database /// - `Err(...)` - Database error occurred /// /// # Example /// ```no_run /// # use adaptive_strategy::database_loader::DatabaseConfigLoader; /// # async fn example(loader: &DatabaseConfigLoader) -> Result<(), Box> { /// let config = loader.load_config("default").await? /// .expect("Default strategy not found"); /// config.validate()?; /// # Ok(()) /// # } /// ``` pub async fn load_config( &self, strategy_id: &str, ) -> Result, String> { // Load main configuration let config_row: Option = sqlx::query_as( r#" SELECT id, strategy_id, name, description, execution_interval_ms, error_backoff_duration_secs, max_concurrent_operations, strategy_timeout_secs, max_parallel_models, rebalancing_interval_secs, min_model_weight, max_model_weight, max_position_size, max_leverage, stop_loss_pct, position_sizing_method, max_portfolio_var, max_drawdown_threshold, kelly_fraction, book_depth, vpin_window, trade_classification_threshold, trade_size_buckets, microstructure_features, regime_detection_method, regime_lookback_window, regime_transition_threshold, regime_features, execution_algorithm, max_order_size, min_order_size, order_timeout_secs, max_slippage_bps, smart_routing_enabled, dark_pool_preference, active, version, created_at, updated_at, created_by, updated_by, metadata FROM adaptive_strategy_config WHERE strategy_id = $1 AND active = true "#, ) .bind(strategy_id) .fetch_optional(&self.pool) .await .map_err(|e| format!("Failed to load config: {}", e))?; let Some(config_row) = config_row else { return Ok(None); }; let config_id = config_row.id; // Load associated models let models: Vec = sqlx::query_as( r#" SELECT id, strategy_config_id, model_id, model_name, model_type, parameters, initial_weight, enabled, display_order, created_at, updated_at FROM adaptive_strategy_models WHERE strategy_config_id = $1 ORDER BY display_order, created_at "#, ) .bind(config_id) .fetch_all(&self.pool) .await .map_err(|e| format!("Failed to load models: {}", e))?; // Load associated features let features: Vec = sqlx::query_as( r#" SELECT id, strategy_config_id, feature_name, feature_type, parameters, enabled, required, created_at, updated_at FROM adaptive_strategy_features WHERE strategy_config_id = $1 ORDER BY feature_name "#, ) .bind(config_id) .fetch_all(&self.pool) .await .map_err(|e| format!("Failed to load features: {}", e))?; // Convert to structured configuration let config = config_row.into_config(models, features)?; // Validate configuration config.validate()?; Ok(Some(config)) } // REMOVED: load_config_or_default() does not make sense for postgres-backed configs // The config_types::AdaptiveStrategyConfig has no Default impl (by design). // Fallback logic should use config::AdaptiveStrategyConfig::default() at the // application level after calling load_config().await. // // For non-postgres builds, see the stub implementation below which provides // the fallback directly to config::AdaptiveStrategyConfig::default(). /// Enable hot-reload support /// /// Subscribes to PostgreSQL NOTIFY events for configuration changes. /// Call `check_for_updates()` periodically to receive notifications. pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> { let mut listener = PgListener::connect_with(&self.pool).await?; listener.listen("adaptive_strategy_config_change").await?; self.listener = Some(listener); Ok(()) } /// Check for configuration change notifications /// /// Returns the strategy_id if a configuration change notification /// was received, or None if no notifications are pending. /// /// # Example /// ```no_run /// # use adaptive_strategy::database_loader::DatabaseConfigLoader; /// # async fn example(loader: &mut DatabaseConfigLoader) -> Result<(), Box> { /// // In a background task /// loop { /// if let Some(strategy_id) = loader.check_for_updates().await? { /// println!("Configuration changed for strategy: {}", strategy_id); /// // Reload configuration /// let new_config = loader.load_config(&strategy_id).await?; /// } /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; /// } /// # } /// ``` pub async fn check_for_updates(&mut self) -> Result, sqlx::Error> { if let Some(listener) = &mut self.listener { if let Some(notification) = listener.try_recv().await? { // Parse notification payload if let Ok(payload) = serde_json::from_str::(notification.payload()) { if let Some(strategy_id) = payload.get("strategy_id").and_then(|v| v.as_str()) { return Ok(Some(strategy_id.to_string())); } } } } Ok(None) } /// Get the underlying connection pool pub fn pool(&self) -> &sqlx::PgPool { &self.pool } } // Synchronous fallback loader for when postgres feature is not enabled #[cfg(not(feature = "postgres"))] pub struct DatabaseConfigLoader; #[cfg(not(feature = "postgres"))] impl DatabaseConfigLoader { /// Always returns default configuration when postgres feature is disabled pub fn load_config_or_default(&self, _strategy_id: &str) -> crate::config::AdaptiveStrategyConfig { crate::config::AdaptiveStrategyConfig::default() } } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn test_fallback_loader_without_postgres() { // When postgres feature is disabled, should compile and return defaults #[cfg(not(feature = "postgres"))] { let loader = DatabaseConfigLoader; let config = loader.load_config_or_default("test"); assert_eq!(config.general.execution_interval, Duration::from_millis(100)); } } }