/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 | | } |