Files
foxhunt/adaptive-strategy/src/database_loader.rs
jgrusewski 00ae84dd88 refactor: remove dead code and #[allow(dead_code)] annotations across workspace
Strip all 413 #[allow(dead_code)] annotations from 139 files and remove
the actual dead code they were suppressing: unused struct fields (and their
constructor sites), unused methods/functions, and entire dead structs.

Key removals:
- trading_engine compliance: ~50 dead structs/fields across audit, reporting, SOX modules
- trading_service: dead execution engine fields, broker routing, paper trading methods
- ml_training_service: dead TLS validation (~340 lines), GPU state, monitoring fields
- backtesting_service: dead model cache, TLS validation, TradeSignal fields
- risk: dead VaR engine fields, safety coordinator fields, position tracker fields
- adaptive-strategy: dead ensemble methods, regime detection, sizing functions

147 files changed, -4264 net lines. Workspace compiles with 0 errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:12:20 +01:00

271 lines
9.5 KiB
Rust

//! 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, FeatureConfigRow, ModelConfigRow,
};
#[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<PgListener>,
/// Cache timeout for loaded configurations (reserved for future caching implementation)
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<dyn std::error::Error>> {
/// let loader = DatabaseConfigLoader::new("postgresql://localhost/foxhunt").await?;
/// # Ok(())
/// # }
/// ```
pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
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<dyn std::error::Error>> {
/// 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<Option<AdaptiveStrategyConfig>, String> {
// Load main configuration
let config_row: Option<AdaptiveStrategyConfigRow> = 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<ModelConfigRow> = 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<FeatureConfigRow> = 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<dyn std::error::Error>> {
/// // 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<Option<String>, 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::<serde_json::Value>(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)
);
}
}
}