Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/runtime.rs
Line
Count
Source
1
//! Runtime configuration layer for environment-aware defaults.
2
//!
3
//! This module provides Tier 2 runtime configuration that complements the
4
//! compile-time constants in `common::thresholds`. Values here can be overridden
5
//! via environment variables to support different deployment environments
6
//! (development, staging, production) without recompilation.
7
//!
8
//! # Architecture
9
//!
10
//! - Tier 1 (Compile-time): `common::thresholds` - Performance-critical constants
11
//! - Tier 2 (Runtime): This module - Environment-aware operational parameters
12
//! - Tier 3 (Database): Hot-reload via PostgreSQL NOTIFY/LISTEN
13
//!
14
//! # Environment Variables
15
//!
16
//! ## Database Configuration
17
//! - `DATABASE_QUERY_TIMEOUT_MS` - Query timeout in milliseconds (default: environment-aware)
18
//! - `DATABASE_CONNECTION_TIMEOUT_MS` - Connection timeout in milliseconds
19
//! - `DATABASE_POOL_SIZE` - Connection pool size
20
//! - `DATABASE_MAX_POOL_SIZE` - Maximum pool size
21
//! - `DATABASE_ACQUIRE_TIMEOUT_MS` - Pool acquire timeout in milliseconds
22
//!
23
//! ## Cache Configuration
24
//! - `CACHE_POSITION_TTL_SECS` - Position cache TTL in seconds
25
//! - `CACHE_VAR_TTL_SECS` - VaR calculation cache TTL in seconds
26
//! - `CACHE_COMPLIANCE_TTL_SECS` - Compliance check cache TTL in seconds
27
//! - `CACHE_MARKET_DATA_TTL_SECS` - Market data cache TTL in seconds
28
//! - `CACHE_MODEL_PREDICTION_TTL_SECS` - Model prediction cache TTL in seconds
29
//!
30
//! ## Network Configuration
31
//! - `NETWORK_GRPC_CONNECT_TIMEOUT_SECS` - gRPC connect timeout in seconds
32
//! - `NETWORK_GRPC_REQUEST_TIMEOUT_SECS` - gRPC request timeout in seconds
33
//! - `NETWORK_KEEP_ALIVE_INTERVAL_SECS` - Keep-alive interval in seconds
34
//! - `NETWORK_KEEP_ALIVE_TIMEOUT_SECS` - Keep-alive timeout in seconds
35
//! - `NETWORK_MAX_CONCURRENT_CONNECTIONS` - Maximum concurrent connections
36
//!
37
//! ## Retry Configuration
38
//! - `RETRY_INITIAL_DELAY_MS` - Initial retry delay in milliseconds
39
//! - `RETRY_MAX_DELAY_SECS` - Maximum retry delay in seconds
40
//! - `RETRY_MAX_ATTEMPTS` - Maximum retry attempts
41
//! - `RETRY_BACKOFF_MULTIPLIER` - Backoff multiplier for exponential backoff
42
//!
43
//! ## Safety Configuration
44
//! - `SAFETY_CHECK_TIMEOUT_MS` - Safety check timeout in milliseconds
45
//! - `SAFETY_AUTO_RECOVERY_DELAY_SECS` - Auto-recovery delay in seconds
46
//! - `SAFETY_LOSS_CHECK_INTERVAL_SECS` - Loss check interval in seconds
47
//! - `SAFETY_POSITION_CHECK_INTERVAL_SECS` - Position check interval in seconds
48
//!
49
//! ## ML Configuration
50
//! - `ML_MAX_BATCH_SIZE` - Maximum batch size for ML inference
51
//! - `ML_INFERENCE_TIMEOUT_MS` - ML inference timeout in milliseconds
52
//! - `ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS` - Model cache cleanup interval
53
//! - `ML_DRIFT_CHECK_INTERVAL_SECS` - Drift detection check interval
54
//!
55
//! ## Risk Configuration
56
//! - `RISK_VAR_LOOKBACK_DAYS` - VaR lookback period in trading days
57
//! - `RISK_VAR_CONFIDENCE` - VaR confidence level (0.0-1.0)
58
//! - `RISK_MAX_DRAWDOWN_WARNING_PCT` - Max drawdown warning threshold
59
//!
60
//! # Example
61
//!
62
//! ```rust,no_run
63
//! use config::runtime::{RuntimeConfig, Environment};
64
//!
65
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
66
//! // Auto-detect environment and load from env vars
67
//! let config = RuntimeConfig::from_env()?;
68
//!
69
//! // Or specify environment explicitly
70
//! let prod_config = RuntimeConfig::from_env_with_environment(Environment::Production)?;
71
//!
72
//! // Or use defaults for specific environment
73
//! let dev_config = RuntimeConfig::with_defaults(Environment::Development);
74
//!
75
//! println!("Database query timeout: {:?}", config.database.query_timeout);
76
//! println!("Position cache TTL: {:?}", config.cache.position_ttl);
77
//! # Ok(())
78
//! # }
79
//! ```
80
81
use crate::error::{ConfigError, ConfigResult};
82
use serde::{Deserialize, Serialize};
83
use std::time::Duration;
84
85
/// Deployment environment enumeration.
86
///
87
/// Determines default values for runtime configuration parameters.
88
/// Different environments have different performance vs safety trade-offs.
89
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90
pub enum Environment {
91
    /// Development environment - Relaxed timeouts, verbose logging
92
    Development,
93
    /// Staging environment - Production-like settings with some debug features
94
    Staging,
95
    /// Production environment - Optimized for performance and reliability
96
    Production,
97
}
98
99
impl Environment {
100
    /// Detects the environment from the ENVIRONMENT environment variable.
101
    ///
102
    /// Falls back to Development if not set or invalid.
103
1
    pub fn detect() -> Self {
104
1
        match std::env::var("ENVIRONMENT")
105
1
            .unwrap_or_else(|_| "development".to_string())
106
1
            .to_lowercase()
107
1
            .as_str()
108
        {
109
1
            "production" | "prod" => 
Environment::Production0
,
110
1
            "staging" | "stage" => 
Environment::Staging0
,
111
1
            _ => Environment::Development,
112
        }
113
1
    }
114
115
    /// Returns true if this is a production environment.
116
3
    pub fn is_production(&self) -> bool {
117
3
        
matches!2
(self, Environment::Production)
118
3
    }
119
120
    /// Returns true if this is a development environment.
121
3
    pub fn is_development(&self) -> bool {
122
3
        
matches!2
(self, Environment::Development)
123
3
    }
124
}
125
126
/// Database runtime configuration.
127
///
128
/// Controls database connection pooling, timeouts, and query execution limits.
129
#[derive(Debug, Clone, Serialize, Deserialize)]
130
pub struct DatabaseRuntimeConfig {
131
    /// Query timeout for standard operations
132
    pub query_timeout: Duration,
133
    /// Connection establishment timeout
134
    pub connection_timeout: Duration,
135
    /// Pool acquire timeout
136
    pub acquire_timeout: Duration,
137
    /// Default pool size
138
    pub pool_size: u32,
139
    /// Maximum pool size
140
    pub max_pool_size: u32,
141
    /// Connection lifetime
142
    pub connection_lifetime: Duration,
143
    /// Idle timeout
144
    pub idle_timeout: Duration,
145
}
146
147
impl DatabaseRuntimeConfig {
148
    /// Creates configuration with environment-aware defaults.
149
10
    pub fn with_defaults(env: Environment) -> Self {
150
10
        match env {
151
3
            Environment::Development => Self {
152
3
                query_timeout: Duration::from_millis(5000), // More relaxed for debugging
153
3
                connection_timeout: Duration::from_millis(500),
154
3
                acquire_timeout: Duration::from_millis(200),
155
3
                pool_size: 10,
156
3
                max_pool_size: 50,
157
3
                connection_lifetime: Duration::from_secs(1800), // 30 minutes
158
3
                idle_timeout: Duration::from_secs(600), // 10 minutes
159
3
            },
160
1
            Environment::Staging => Self {
161
1
                query_timeout: Duration::from_millis(2000),
162
1
                connection_timeout: Duration::from_millis(200),
163
1
                acquire_timeout: Duration::from_millis(100),
164
1
                pool_size: 15,
165
1
                max_pool_size: 75,
166
1
                connection_lifetime: Duration::from_secs(3600), // 1 hour
167
1
                idle_timeout: Duration::from_secs(300), // 5 minutes
168
1
            },
169
6
            Environment::Production => Self {
170
6
                query_timeout: Duration::from_millis(1000), // Tight timeout for HFT
171
6
                connection_timeout: Duration::from_millis(100),
172
6
                acquire_timeout: Duration::from_millis(50),
173
6
                pool_size: 20,
174
6
                max_pool_size: 100,
175
6
                connection_lifetime: Duration::from_secs(3600), // 1 hour
176
6
                idle_timeout: Duration::from_secs(300), // 5 minutes
177
6
            },
178
        }
179
10
    }
180
181
    /// Loads from environment variables with fallback to defaults.
182
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
183
0
        let defaults = Self::with_defaults(env);
184
185
        Ok(Self {
186
0
            query_timeout: parse_env_duration_ms("DATABASE_QUERY_TIMEOUT_MS", defaults.query_timeout)?,
187
0
            connection_timeout: parse_env_duration_ms("DATABASE_CONNECTION_TIMEOUT_MS", defaults.connection_timeout)?,
188
0
            acquire_timeout: parse_env_duration_ms("DATABASE_ACQUIRE_TIMEOUT_MS", defaults.acquire_timeout)?,
189
0
            pool_size: parse_env_u32("DATABASE_POOL_SIZE", defaults.pool_size)?,
190
0
            max_pool_size: parse_env_u32("DATABASE_MAX_POOL_SIZE", defaults.max_pool_size)?,
191
0
            connection_lifetime: parse_env_duration_secs("DATABASE_CONNECTION_LIFETIME_SECS", defaults.connection_lifetime)?,
192
0
            idle_timeout: parse_env_duration_secs("DATABASE_IDLE_TIMEOUT_SECS", defaults.idle_timeout)?,
193
        })
194
0
    }
195
196
    /// Validates the configuration.
197
5
    pub fn validate(&self) -> ConfigResult<()> {
198
5
        if self.query_timeout.as_millis() == 0 {
199
1
            return Err(ConfigError::Invalid("Query timeout must be positive".into()));
200
4
        }
201
4
        if self.pool_size == 0 {
202
1
            return Err(ConfigError::Invalid("Pool size must be positive".into()));
203
3
        }
204
3
        if self.pool_size > self.max_pool_size {
205
1
            return Err(ConfigError::Invalid("Pool size cannot exceed max pool size".into()));
206
2
        }
207
2
        Ok(())
208
5
    }
209
}
210
211
/// Cache TTL runtime configuration.
212
///
213
/// Controls time-to-live values for various cache types.
214
#[derive(Debug, Clone, Serialize, Deserialize)]
215
pub struct CacheRuntimeConfig {
216
    /// Position cache TTL
217
    pub position_ttl: Duration,
218
    /// VaR calculation cache TTL
219
    pub var_ttl: Duration,
220
    /// Compliance check cache TTL
221
    pub compliance_ttl: Duration,
222
    /// Market data cache TTL
223
    pub market_data_ttl: Duration,
224
    /// Model prediction cache TTL
225
    pub model_prediction_ttl: Duration,
226
}
227
228
impl CacheRuntimeConfig {
229
    /// Creates configuration with environment-aware defaults.
230
8
    pub fn with_defaults(env: Environment) -> Self {
231
8
        match env {
232
3
            Environment::Development => Self {
233
3
                position_ttl: Duration::from_secs(120), // Longer TTL for debugging
234
3
                var_ttl: Duration::from_secs(7200), // 2 hours
235
3
                compliance_ttl: Duration::from_secs(172800), // 48 hours
236
3
                market_data_ttl: Duration::from_secs(600), // 10 minutes
237
3
                model_prediction_ttl: Duration::from_secs(120), // 2 minutes
238
3
            },
239
1
            Environment::Staging => Self {
240
1
                position_ttl: Duration::from_secs(90),
241
1
                var_ttl: Duration::from_secs(5400), // 1.5 hours
242
1
                compliance_ttl: Duration::from_secs(129600), // 36 hours
243
1
                market_data_ttl: Duration::from_secs(450), // 7.5 minutes
244
1
                model_prediction_ttl: Duration::from_secs(90),
245
1
            },
246
4
            Environment::Production => Self {
247
4
                position_ttl: Duration::from_secs(60), // 1 minute for HFT
248
4
                var_ttl: Duration::from_secs(3600), // 1 hour
249
4
                compliance_ttl: Duration::from_secs(86400), // 24 hours
250
4
                market_data_ttl: Duration::from_secs(300), // 5 minutes
251
4
                model_prediction_ttl: Duration::from_secs(60), // 1 minute
252
4
            },
253
        }
254
8
    }
255
256
    /// Loads from environment variables with fallback to defaults.
257
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
258
0
        let defaults = Self::with_defaults(env);
259
260
        Ok(Self {
261
0
            position_ttl: parse_env_duration_secs("CACHE_POSITION_TTL_SECS", defaults.position_ttl)?,
262
0
            var_ttl: parse_env_duration_secs("CACHE_VAR_TTL_SECS", defaults.var_ttl)?,
263
0
            compliance_ttl: parse_env_duration_secs("CACHE_COMPLIANCE_TTL_SECS", defaults.compliance_ttl)?,
264
0
            market_data_ttl: parse_env_duration_secs("CACHE_MARKET_DATA_TTL_SECS", defaults.market_data_ttl)?,
265
0
            model_prediction_ttl: parse_env_duration_secs("CACHE_MODEL_PREDICTION_TTL_SECS", defaults.model_prediction_ttl)?,
266
        })
267
0
    }
268
269
    /// Validates the configuration.
270
3
    pub fn validate(&self) -> ConfigResult<()> {
271
3
        if self.position_ttl.as_secs() == 0 {
272
1
            return Err(ConfigError::Invalid("Position TTL must be positive".into()));
273
2
        }
274
2
        if self.var_ttl.as_secs() == 0 {
275
0
            return Err(ConfigError::Invalid("VaR TTL must be positive".into()));
276
2
        }
277
2
        Ok(())
278
3
    }
279
}
280
281
/// Network timeout runtime configuration.
282
///
283
/// Controls gRPC and network-related timeouts.
284
#[derive(Debug, Clone, Serialize, Deserialize)]
285
pub struct TimeoutConfig {
286
    /// gRPC connect timeout
287
    pub grpc_connect_timeout: Duration,
288
    /// gRPC request timeout
289
    pub grpc_request_timeout: Duration,
290
    /// Keep-alive interval
291
    pub keep_alive_interval: Duration,
292
    /// Keep-alive timeout
293
    pub keep_alive_timeout: Duration,
294
    /// Maximum concurrent connections
295
    pub max_concurrent_connections: u32,
296
}
297
298
impl TimeoutConfig {
299
    /// Creates configuration with environment-aware defaults.
300
7
    pub fn with_defaults(env: Environment) -> Self {
301
7
        match env {
302
3
            Environment::Development => Self {
303
3
                grpc_connect_timeout: Duration::from_secs(10),
304
3
                grpc_request_timeout: Duration::from_secs(30),
305
3
                keep_alive_interval: Duration::from_secs(60),
306
3
                keep_alive_timeout: Duration::from_secs(10),
307
3
                max_concurrent_connections: 50,
308
3
            },
309
1
            Environment::Staging => Self {
310
1
                grpc_connect_timeout: Duration::from_secs(7),
311
1
                grpc_request_timeout: Duration::from_secs(20),
312
1
                keep_alive_interval: Duration::from_secs(45),
313
1
                keep_alive_timeout: Duration::from_secs(7),
314
1
                max_concurrent_connections: 75,
315
1
            },
316
3
            Environment::Production => Self {
317
3
                grpc_connect_timeout: Duration::from_secs(5),
318
3
                grpc_request_timeout: Duration::from_secs(10),
319
3
                keep_alive_interval: Duration::from_secs(30),
320
3
                keep_alive_timeout: Duration::from_secs(5),
321
3
                max_concurrent_connections: 100,
322
3
            },
323
        }
324
7
    }
325
326
    /// Loads from environment variables with fallback to defaults.
327
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
328
0
        let defaults = Self::with_defaults(env);
329
330
        Ok(Self {
331
0
            grpc_connect_timeout: parse_env_duration_secs("NETWORK_GRPC_CONNECT_TIMEOUT_SECS", defaults.grpc_connect_timeout)?,
332
0
            grpc_request_timeout: parse_env_duration_secs("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", defaults.grpc_request_timeout)?,
333
0
            keep_alive_interval: parse_env_duration_secs("NETWORK_KEEP_ALIVE_INTERVAL_SECS", defaults.keep_alive_interval)?,
334
0
            keep_alive_timeout: parse_env_duration_secs("NETWORK_KEEP_ALIVE_TIMEOUT_SECS", defaults.keep_alive_timeout)?,
335
0
            max_concurrent_connections: parse_env_u32("NETWORK_MAX_CONCURRENT_CONNECTIONS", defaults.max_concurrent_connections)?,
336
        })
337
0
    }
338
339
    /// Validates the configuration.
340
1
    pub fn validate(&self) -> ConfigResult<()> {
341
1
        if self.grpc_connect_timeout.as_secs() == 0 {
342
0
            return Err(ConfigError::Invalid("gRPC connect timeout must be positive".into()));
343
1
        }
344
1
        if self.max_concurrent_connections == 0 {
345
0
            return Err(ConfigError::Invalid("Max concurrent connections must be positive".into()));
346
1
        }
347
1
        Ok(())
348
1
    }
349
}
350
351
/// Operational limits runtime configuration.
352
///
353
/// Controls retry behavior, safety checks, ML parameters, and risk calculations.
354
#[derive(Debug, Clone, Serialize, Deserialize)]
355
pub struct LimitsConfig {
356
    // Retry configuration
357
    /// Initial retry delay
358
    pub retry_initial_delay: Duration,
359
    /// Maximum retry delay
360
    pub retry_max_delay: Duration,
361
    /// Maximum retry attempts
362
    pub retry_max_attempts: u32,
363
    /// Backoff multiplier
364
    pub retry_backoff_multiplier: f32,
365
366
    // Safety configuration
367
    /// Safety check timeout
368
    pub safety_check_timeout: Duration,
369
    /// Auto-recovery delay
370
    pub safety_auto_recovery_delay: Duration,
371
    /// Loss check interval
372
    pub safety_loss_check_interval: Duration,
373
    /// Position check interval
374
    pub safety_position_check_interval: Duration,
375
376
    // ML configuration
377
    /// Maximum batch size for ML inference
378
    pub ml_max_batch_size: usize,
379
    /// ML inference timeout
380
    pub ml_inference_timeout: Duration,
381
    /// Model cache cleanup interval
382
    pub ml_cache_cleanup_interval: Duration,
383
    /// Drift detection check interval
384
    pub ml_drift_check_interval: Duration,
385
386
    // Risk configuration
387
    /// VaR lookback period in trading days
388
    pub risk_var_lookback_days: usize,
389
    /// VaR confidence level
390
    pub risk_var_confidence: f64,
391
    /// Max drawdown warning threshold (percentage)
392
    pub risk_max_drawdown_warning_pct: u8,
393
}
394
395
impl LimitsConfig {
396
    /// Creates configuration with environment-aware defaults.
397
10
    pub fn with_defaults(env: Environment) -> Self {
398
10
        match env {
399
3
            Environment::Development => Self {
400
3
                // Retry
401
3
                retry_initial_delay: Duration::from_millis(200),
402
3
                retry_max_delay: Duration::from_secs(60),
403
3
                retry_max_attempts: 5,
404
3
                retry_backoff_multiplier: 2.0,
405
3
406
3
                // Safety
407
3
                safety_check_timeout: Duration::from_millis(50),
408
3
                safety_auto_recovery_delay: Duration::from_secs(60),
409
3
                safety_loss_check_interval: Duration::from_secs(30),
410
3
                safety_position_check_interval: Duration::from_secs(15),
411
3
412
3
                // ML
413
3
                ml_max_batch_size: 1024,
414
3
                ml_inference_timeout: Duration::from_millis(200),
415
3
                ml_cache_cleanup_interval: Duration::from_secs(7200), // 2 hours
416
3
                ml_drift_check_interval: Duration::from_secs(600), // 10 minutes
417
3
418
3
                // Risk
419
3
                risk_var_lookback_days: 252,
420
3
                risk_var_confidence: 0.95,
421
3
                risk_max_drawdown_warning_pct: 20,
422
3
            },
423
1
            Environment::Staging => Self {
424
1
                // Retry
425
1
                retry_initial_delay: Duration::from_millis(150),
426
1
                retry_max_delay: Duration::from_secs(45),
427
1
                retry_max_attempts: 4,
428
1
                retry_backoff_multiplier: 1.75,
429
1
430
1
                // Safety
431
1
                safety_check_timeout: Duration::from_millis(25),
432
1
                safety_auto_recovery_delay: Duration::from_secs(900), // 15 minutes
433
1
                safety_loss_check_interval: Duration::from_secs(15),
434
1
                safety_position_check_interval: Duration::from_secs(7),
435
1
436
1
                // ML
437
1
                ml_max_batch_size: 4096,
438
1
                ml_inference_timeout: Duration::from_millis(150),
439
1
                ml_cache_cleanup_interval: Duration::from_secs(5400), // 1.5 hours
440
1
                ml_drift_check_interval: Duration::from_secs(450), // 7.5 minutes
441
1
442
1
                // Risk
443
1
                risk_var_lookback_days: 252,
444
1
                risk_var_confidence: 0.95,
445
1
                risk_max_drawdown_warning_pct: 17,
446
1
            },
447
6
            Environment::Production => Self {
448
6
                // Retry
449
6
                retry_initial_delay: Duration::from_millis(100),
450
6
                retry_max_delay: Duration::from_secs(30),
451
6
                retry_max_attempts: 3,
452
6
                retry_backoff_multiplier: 1.5,
453
6
454
6
                // Safety
455
6
                safety_check_timeout: Duration::from_millis(5),
456
6
                safety_auto_recovery_delay: Duration::from_secs(1800), // 30 minutes
457
6
                safety_loss_check_interval: Duration::from_secs(5),
458
6
                safety_position_check_interval: Duration::from_secs(2),
459
6
460
6
                // ML
461
6
                ml_max_batch_size: 8192,
462
6
                ml_inference_timeout: Duration::from_millis(100),
463
6
                ml_cache_cleanup_interval: Duration::from_secs(3600), // 1 hour
464
6
                ml_drift_check_interval: Duration::from_secs(300), // 5 minutes
465
6
466
6
                // Risk
467
6
                risk_var_lookback_days: 252,
468
6
                risk_var_confidence: 0.95,
469
6
                risk_max_drawdown_warning_pct: 15,
470
6
            },
471
        }
472
10
    }
473
474
    /// Loads from environment variables with fallback to defaults.
475
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
476
0
        let defaults = Self::with_defaults(env);
477
478
        Ok(Self {
479
            // Retry
480
0
            retry_initial_delay: parse_env_duration_ms("RETRY_INITIAL_DELAY_MS", defaults.retry_initial_delay)?,
481
0
            retry_max_delay: parse_env_duration_secs("RETRY_MAX_DELAY_SECS", defaults.retry_max_delay)?,
482
0
            retry_max_attempts: parse_env_u32("RETRY_MAX_ATTEMPTS", defaults.retry_max_attempts)?,
483
0
            retry_backoff_multiplier: parse_env_f32("RETRY_BACKOFF_MULTIPLIER", defaults.retry_backoff_multiplier)?,
484
485
            // Safety
486
0
            safety_check_timeout: parse_env_duration_ms("SAFETY_CHECK_TIMEOUT_MS", defaults.safety_check_timeout)?,
487
0
            safety_auto_recovery_delay: parse_env_duration_secs("SAFETY_AUTO_RECOVERY_DELAY_SECS", defaults.safety_auto_recovery_delay)?,
488
0
            safety_loss_check_interval: parse_env_duration_secs("SAFETY_LOSS_CHECK_INTERVAL_SECS", defaults.safety_loss_check_interval)?,
489
0
            safety_position_check_interval: parse_env_duration_secs("SAFETY_POSITION_CHECK_INTERVAL_SECS", defaults.safety_position_check_interval)?,
490
491
            // ML
492
0
            ml_max_batch_size: parse_env_usize("ML_MAX_BATCH_SIZE", defaults.ml_max_batch_size)?,
493
0
            ml_inference_timeout: parse_env_duration_ms("ML_INFERENCE_TIMEOUT_MS", defaults.ml_inference_timeout)?,
494
0
            ml_cache_cleanup_interval: parse_env_duration_secs("ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS", defaults.ml_cache_cleanup_interval)?,
495
0
            ml_drift_check_interval: parse_env_duration_secs("ML_DRIFT_CHECK_INTERVAL_SECS", defaults.ml_drift_check_interval)?,
496
497
            // Risk
498
0
            risk_var_lookback_days: parse_env_usize("RISK_VAR_LOOKBACK_DAYS", defaults.risk_var_lookback_days)?,
499
0
            risk_var_confidence: parse_env_f64("RISK_VAR_CONFIDENCE", defaults.risk_var_confidence)?,
500
0
            risk_max_drawdown_warning_pct: parse_env_u8("RISK_MAX_DRAWDOWN_WARNING_PCT", defaults.risk_max_drawdown_warning_pct)?,
501
        })
502
0
    }
503
504
    /// Validates the configuration.
505
5
    pub fn validate(&self) -> ConfigResult<()> {
506
5
        if self.retry_max_attempts == 0 {
507
1
            return Err(ConfigError::Invalid("Retry max attempts must be positive".into()));
508
4
        }
509
4
        if self.retry_backoff_multiplier <= 1.0 {
510
1
            return Err(ConfigError::Invalid("Backoff multiplier must be > 1.0".into()));
511
3
        }
512
3
        if self.ml_max_batch_size == 0 {
513
0
            return Err(ConfigError::Invalid("ML max batch size must be positive".into()));
514
3
        }
515
3
        if self.risk_var_confidence < 0.0 || self.risk_var_confidence > 1.0 {
516
1
            return Err(ConfigError::Invalid("VaR confidence must be between 0.0 and 1.0".into()));
517
2
        }
518
2
        if self.risk_var_lookback_days == 0 {
519
0
            return Err(ConfigError::Invalid("VaR lookback days must be positive".into()));
520
2
        }
521
2
        Ok(())
522
5
    }
523
}
524
525
/// Complete runtime configuration for the Foxhunt trading system.
526
///
527
/// Aggregates all runtime configuration categories with environment-aware defaults
528
/// and environment variable overrides.
529
#[derive(Debug, Clone, Serialize, Deserialize)]
530
pub struct RuntimeConfig {
531
    /// Detected or specified environment
532
    pub environment: Environment,
533
    /// Database configuration
534
    pub database: DatabaseRuntimeConfig,
535
    /// Cache configuration
536
    pub cache: CacheRuntimeConfig,
537
    /// Timeout configuration
538
    pub timeouts: TimeoutConfig,
539
    /// Limits and operational parameters
540
    pub limits: LimitsConfig,
541
}
542
543
impl RuntimeConfig {
544
    /// Creates runtime configuration by auto-detecting environment and loading from env vars.
545
    ///
546
    /// # Errors
547
    ///
548
    /// Returns ConfigError if environment variables contain invalid values or
549
    /// if validation fails.
550
0
    pub fn from_env() -> ConfigResult<Self> {
551
0
        let environment = Environment::detect();
552
0
        Self::from_env_with_environment(environment)
553
0
    }
554
555
    /// Creates runtime configuration with specified environment and loads from env vars.
556
    ///
557
    /// # Arguments
558
    ///
559
    /// * `environment` - The deployment environment to use for defaults
560
    ///
561
    /// # Errors
562
    ///
563
    /// Returns ConfigError if environment variables contain invalid values or
564
    /// if validation fails.
565
0
    pub fn from_env_with_environment(environment: Environment) -> ConfigResult<Self> {
566
0
        let config = Self {
567
0
            environment,
568
0
            database: DatabaseRuntimeConfig::from_env(environment)?,
569
0
            cache: CacheRuntimeConfig::from_env(environment)?,
570
0
            timeouts: TimeoutConfig::from_env(environment)?,
571
0
            limits: LimitsConfig::from_env(environment)?,
572
        };
573
574
0
        config.validate()?;
575
0
        Ok(config)
576
0
    }
577
578
    /// Creates runtime configuration with environment-specific defaults.
579
    ///
580
    /// Does not read from environment variables. Useful for testing or
581
    /// when you want pure default values.
582
    ///
583
    /// # Arguments
584
    ///
585
    /// * `environment` - The deployment environment to use for defaults
586
5
    pub fn with_defaults(environment: Environment) -> Self {
587
5
        Self {
588
5
            environment,
589
5
            database: DatabaseRuntimeConfig::with_defaults(environment),
590
5
            cache: CacheRuntimeConfig::with_defaults(environment),
591
5
            timeouts: TimeoutConfig::with_defaults(environment),
592
5
            limits: LimitsConfig::with_defaults(environment),
593
5
        }
594
5
    }
595
596
    /// Validates the entire runtime configuration.
597
    ///
598
    /// # Errors
599
    ///
600
    /// Returns ConfigError if any configuration values are invalid.
601
1
    pub fn validate(&self) -> ConfigResult<()> {
602
1
        self.database.validate()
?0
;
603
1
        self.cache.validate()
?0
;
604
1
        self.timeouts.validate()
?0
;
605
1
        self.limits.validate()
?0
;
606
1
        Ok(())
607
1
    }
608
}
609
610
// Helper functions for parsing environment variables
611
612
0
fn parse_env_duration_ms(key: &str, default: Duration) -> ConfigResult<Duration> {
613
0
    match std::env::var(key) {
614
0
        Ok(val) => {
615
0
            let ms = val.parse::<u64>()
616
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
617
0
            Ok(Duration::from_millis(ms))
618
        }
619
0
        Err(_) => Ok(default),
620
    }
621
0
}
622
623
0
fn parse_env_duration_secs(key: &str, default: Duration) -> ConfigResult<Duration> {
624
0
    match std::env::var(key) {
625
0
        Ok(val) => {
626
0
            let secs = val.parse::<u64>()
627
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
628
0
            Ok(Duration::from_secs(secs))
629
        }
630
0
        Err(_) => Ok(default),
631
    }
632
0
}
633
634
0
fn parse_env_u32(key: &str, default: u32) -> ConfigResult<u32> {
635
0
    match std::env::var(key) {
636
0
        Ok(val) => val.parse::<u32>()
637
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u32 for {}: {}", key, e))),
638
0
        Err(_) => Ok(default),
639
    }
640
0
}
641
642
0
fn parse_env_u8(key: &str, default: u8) -> ConfigResult<u8> {
643
0
    match std::env::var(key) {
644
0
        Ok(val) => val.parse::<u8>()
645
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u8 for {}: {}", key, e))),
646
0
        Err(_) => Ok(default),
647
    }
648
0
}
649
650
0
fn parse_env_usize(key: &str, default: usize) -> ConfigResult<usize> {
651
0
    match std::env::var(key) {
652
0
        Ok(val) => val.parse::<usize>()
653
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid usize for {}: {}", key, e))),
654
0
        Err(_) => Ok(default),
655
    }
656
0
}
657
658
0
fn parse_env_f32(key: &str, default: f32) -> ConfigResult<f32> {
659
0
    match std::env::var(key) {
660
0
        Ok(val) => val.parse::<f32>()
661
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f32 for {}: {}", key, e))),
662
0
        Err(_) => Ok(default),
663
    }
664
0
}
665
666
0
fn parse_env_f64(key: &str, default: f64) -> ConfigResult<f64> {
667
0
    match std::env::var(key) {
668
0
        Ok(val) => val.parse::<f64>()
669
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f64 for {}: {}", key, e))),
670
0
        Err(_) => Ok(default),
671
    }
672
0
}
673
674
#[cfg(test)]
675
mod tests {
676
    use super::*;
677
678
    #[test]
679
1
    fn test_environment_detection() {
680
        // Should default to Development
681
1
        let env = Environment::detect();
682
1
        assert!(
matches!0
(env, Environment::Development | Environment::Production | Environment::Staging));
683
1
    }
684
685
    #[test]
686
1
    fn test_environment_is_production() {
687
1
        assert!(Environment::Production.is_production());
688
1
        assert!(!Environment::Development.is_production());
689
1
        assert!(!Environment::Staging.is_production());
690
1
    }
691
692
    #[test]
693
1
    fn test_environment_is_development() {
694
1
        assert!(Environment::Development.is_development());
695
1
        assert!(!Environment::Production.is_development());
696
1
        assert!(!Environment::Staging.is_development());
697
1
    }
698
699
    #[test]
700
1
    fn test_runtime_config_with_defaults() {
701
1
        let config = RuntimeConfig::with_defaults(Environment::Production);
702
1
        assert_eq!(config.environment, Environment::Production);
703
1
        assert!(config.database.query_timeout.as_millis() > 0);
704
1
        assert!(config.cache.position_ttl.as_secs() > 0);
705
1
    }
706
707
    #[test]
708
1
    fn test_runtime_config_validation() {
709
1
        let config = RuntimeConfig::with_defaults(Environment::Development);
710
1
        assert!(config.validate().is_ok());
711
1
    }
712
713
    #[test]
714
1
    fn test_database_config_defaults() {
715
1
        let dev_config = DatabaseRuntimeConfig::with_defaults(Environment::Development);
716
1
        let prod_config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
717
718
        // Production should have tighter timeouts
719
1
        assert!(prod_config.query_timeout < dev_config.query_timeout);
720
1
        assert!(prod_config.connection_timeout < dev_config.connection_timeout);
721
1
    }
722
723
    #[test]
724
1
    fn test_cache_config_defaults() {
725
1
        let dev_config = CacheRuntimeConfig::with_defaults(Environment::Development);
726
1
        let prod_config = CacheRuntimeConfig::with_defaults(Environment::Production);
727
728
        // Production should have shorter TTLs for HFT
729
1
        assert!(prod_config.position_ttl < dev_config.position_ttl);
730
1
        assert!(prod_config.var_ttl < dev_config.var_ttl);
731
1
    }
732
733
    #[test]
734
1
    fn test_timeout_config_defaults() {
735
1
        let dev_config = TimeoutConfig::with_defaults(Environment::Development);
736
1
        let prod_config = TimeoutConfig::with_defaults(Environment::Production);
737
738
        // Production should have tighter timeouts
739
1
        assert!(prod_config.grpc_request_timeout < dev_config.grpc_request_timeout);
740
1
        assert!(prod_config.grpc_connect_timeout < dev_config.grpc_connect_timeout);
741
1
    }
742
743
    #[test]
744
1
    fn test_limits_config_defaults() {
745
1
        let dev_config = LimitsConfig::with_defaults(Environment::Development);
746
1
        let prod_config = LimitsConfig::with_defaults(Environment::Production);
747
748
        // Production should have more aggressive settings
749
1
        assert!(prod_config.safety_check_timeout < dev_config.safety_check_timeout);
750
1
        assert!(prod_config.ml_inference_timeout < dev_config.ml_inference_timeout);
751
1
    }
752
753
    #[test]
754
1
    fn test_database_config_validation() {
755
1
        let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
756
1
        assert!(config.validate().is_ok());
757
758
1
        config.query_timeout = Duration::from_millis(0);
759
1
        assert!(config.validate().is_err());
760
761
1
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
762
1
        config.pool_size = 0;
763
1
        assert!(config.validate().is_err());
764
765
1
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
766
1
        config.pool_size = 200;
767
1
        config.max_pool_size = 100;
768
1
        assert!(config.validate().is_err());
769
1
    }
770
771
    #[test]
772
1
    fn test_cache_config_validation() {
773
1
        let mut config = CacheRuntimeConfig::with_defaults(Environment::Production);
774
1
        assert!(config.validate().is_ok());
775
776
1
        config.position_ttl = Duration::from_secs(0);
777
1
        assert!(config.validate().is_err());
778
1
    }
779
780
    #[test]
781
1
    fn test_limits_config_validation() {
782
1
        let mut config = LimitsConfig::with_defaults(Environment::Production);
783
1
        assert!(config.validate().is_ok());
784
785
1
        config.retry_max_attempts = 0;
786
1
        assert!(config.validate().is_err());
787
788
1
        config = LimitsConfig::with_defaults(Environment::Production);
789
1
        config.retry_backoff_multiplier = 0.5;
790
1
        assert!(config.validate().is_err());
791
792
1
        config = LimitsConfig::with_defaults(Environment::Production);
793
1
        config.risk_var_confidence = 1.5;
794
1
        assert!(config.validate().is_err());
795
1
    }
796
797
    #[test]
798
1
    fn test_staging_environment_defaults() {
799
1
        let config = RuntimeConfig::with_defaults(Environment::Staging);
800
801
        // Staging should be between dev and prod
802
1
        let dev_config = RuntimeConfig::with_defaults(Environment::Development);
803
1
        let prod_config = RuntimeConfig::with_defaults(Environment::Production);
804
805
1
        assert!(config.database.query_timeout > prod_config.database.query_timeout);
806
1
        assert!(config.database.query_timeout < dev_config.database.query_timeout);
807
1
    }
808
}