velora_core/config/mod.rs
1//! Configuration management for the Velora trading platform.
2//!
3//! This module provides comprehensive configuration support with multiple sources:
4//! - TOML configuration files
5//! - Environment variables
6//! - Default values
7//!
8//! Configuration is loaded in the following priority (highest to lowest):
9//! 1. Environment variables (VELORA_*)
10//! 2. Configuration file
11//! 3. Default values
12//!
13//! # Using gonfig
14//!
15//! The configuration system uses the `gonfig` crate for unified configuration management.
16//! Simply call `VeloraConfig::from_gonfig()` to load from all sources automatically.
17//!
18//! # Module Structure
19//!
20//! - [`database`] - Database backend configuration (QuestDB, TimescaleDB, InMemory)
21//! - [`engine`] - Trading engine configuration (Backtesting, Live Trading)
22//! - [`exchange`] - Exchange connection and rate limiting
23//! - [`risk`] - Risk management limits and constraints
24//! - [`logging`] - Logging levels and output configuration
25//!
26//! # Examples
27//!
28//! ```no_run
29//! use velora_core::VeloraConfig;
30//!
31//! // Method 1: Auto-load from all sources (recommended)
32//! let config = VeloraConfig::from_gonfig().unwrap();
33//!
34//! // Method 2: Load from single file
35//! let config = VeloraConfig::from_file("config.toml").unwrap();
36//!
37//! // Method 3: Layer multiple configs
38//! let config = VeloraConfig::from_files(&[
39//! "config/base.toml",
40//! "config/testing.toml",
41//! ]).unwrap();
42//!
43//! // Always validate after loading
44//! config.validate().unwrap();
45//! ```
46
47mod database;
48mod engine;
49mod exchange;
50mod logging;
51mod risk;
52
53// Re-export all config types
54pub use database::{DatabaseBackend, DatabaseConfig, QuestDbConfig, TimescaleDbConfig};
55pub use engine::{BacktestConfig, EngineConfig, LiveTradingConfig};
56pub use exchange::{ExchangeConfig, RateLimitConfig};
57pub use logging::LoggingConfig;
58pub use risk::RiskConfig;
59
60use gonfig::Gonfig;
61use serde::{Deserialize, Serialize};
62use std::collections::HashMap;
63
64use crate::errors::{Result, VeloraError};
65
66/// Main configuration for the Velora platform.
67///
68/// Environment variables can override any setting using the pattern:
69/// `VELORA_<SECTION>_<KEY>` (e.g., `VELORA_ENGINE_LIVE_DRY_RUN=false`)
70///
71/// # Examples
72///
73/// ```no_run
74/// use velora_core::VeloraConfig;
75///
76/// // Load from all sources (files + env vars)
77/// let config = VeloraConfig::from_gonfig().unwrap();
78/// ```
79#[allow(missing_docs)]
80#[derive(Debug, Clone, Serialize, Deserialize, Default, Gonfig)]
81#[Gonfig(env_prefix = "VELORA")]
82pub struct VeloraConfig {
83 /// Exchange configurations
84 /// Env: VELORA_EXCHANGES_* (complex, prefer config file)
85 #[serde(default)]
86 pub exchanges: HashMap<String, ExchangeConfig>,
87
88 /// Engine configuration
89 /// Env: VELORA_ENGINE_*
90 #[serde(default)]
91 pub engine: EngineConfig,
92
93 /// Risk management configuration
94 /// Env: VELORA_RISK_*
95 #[serde(default)]
96 pub risk: RiskConfig,
97
98 /// Logging configuration
99 /// Env: VELORA_LOGGING_*
100 #[serde(default)]
101 pub logging: LoggingConfig,
102
103 /// Database configuration
104 /// Env: VELORA_DATABASE_*
105 #[serde(default)]
106 pub database: DatabaseConfig,
107}
108
109impl VeloraConfig {
110 /// Load configuration from a single TOML file with environment variable overrides.
111 ///
112 /// This is a convenience wrapper around gonfig's ConfigBuilder.
113 ///
114 /// # Examples
115 ///
116 /// ```no_run
117 /// use velora_core::VeloraConfig;
118 ///
119 /// let config = VeloraConfig::from_file("config.toml").unwrap();
120 /// ```
121 pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
122 use gonfig::ConfigBuilder;
123
124 let config = ConfigBuilder::new()
125 .with_file(
126 path.as_ref()
127 .to_str()
128 .ok_or_else(|| VeloraError::ConfigError("Invalid file path".to_string()))?,
129 )?
130 .with_env("VELORA")
131 .build::<Self>()?;
132
133 Ok(config)
134 }
135
136 /// Load configuration from multiple layered TOML files.
137 ///
138 /// Files are loaded in order, with later files overriding earlier ones.
139 /// Environment variables override all file-based configuration.
140 ///
141 /// # Priority (highest to lowest)
142 /// 1. Environment variables (VELORA_*)
143 /// 2. Last config file in the list
144 /// 3. ...
145 /// 4. First config file in the list
146 /// 5. Default values
147 ///
148 /// # Examples
149 ///
150 /// ```no_run
151 /// use velora_core::VeloraConfig;
152 ///
153 /// // Layer environment-specific config over base config
154 /// let config = VeloraConfig::from_files(&[
155 /// "config/base.toml",
156 /// "config/paper-trading.toml",
157 /// ]).unwrap();
158 /// ```
159 pub fn from_files<P: AsRef<std::path::Path>>(paths: &[P]) -> Result<Self> {
160 use gonfig::{ConfigBuilder, MergeStrategy};
161
162 let mut builder = ConfigBuilder::new().with_merge_strategy(MergeStrategy::Deep);
163
164 // Add each file in order
165 for path in paths {
166 builder = builder.with_file(
167 path.as_ref()
168 .to_str()
169 .ok_or_else(|| VeloraError::ConfigError("Invalid file path".to_string()))?,
170 )?;
171 }
172
173 // Environment variables have highest priority
174 let config = builder.with_env("VELORA").build::<Self>()?;
175
176 Ok(config)
177 }
178
179 /// Load configuration with optional files.
180 ///
181 /// Similar to `from_files`, but allows some files to be optional.
182 /// Useful for optional local overrides.
183 ///
184 /// # Examples
185 ///
186 /// ```no_run
187 /// use velora_core::VeloraConfig;
188 ///
189 /// // base.toml is required, local.toml is optional
190 /// let config = VeloraConfig::from_files_optional(&[
191 /// ("config/base.toml", true), // required
192 /// ("config/paper-trading.toml", true), // required
193 /// ("config/local.toml", false), // optional override
194 /// ]).unwrap();
195 /// ```
196 pub fn from_files_optional<P: AsRef<std::path::Path>>(paths: &[(P, bool)]) -> Result<Self> {
197 use gonfig::{ConfigBuilder, MergeStrategy};
198
199 let mut builder = ConfigBuilder::new().with_merge_strategy(MergeStrategy::Deep);
200
201 for (path, required) in paths {
202 let path_str = path
203 .as_ref()
204 .to_str()
205 .ok_or_else(|| VeloraError::ConfigError("Invalid file path".to_string()))?;
206
207 if *required {
208 builder = builder.with_file(path_str)?;
209 } else {
210 builder = builder.with_file_optional(path_str)?;
211 }
212 }
213
214 let config = builder.with_env("VELORA").build::<Self>()?;
215
216 Ok(config)
217 }
218
219 /// Validate configuration values.
220 ///
221 /// Checks that all configuration values are within acceptable ranges
222 /// and meet business logic requirements.
223 ///
224 /// # Errors
225 ///
226 /// Returns an error if:
227 /// - Initial capital is not positive
228 /// - Commission rate is not between 0 and 1
229 /// - Max drawdown is not between 0 and 100
230 pub fn validate(&self) -> Result<()> {
231 // Validate engine config
232 if self.engine.backtest.initial_capital <= 0.0 {
233 return Err(VeloraError::ConfigError(
234 "Initial capital must be positive".to_string(),
235 ));
236 }
237
238 if self.engine.backtest.commission_rate < 0.0 || self.engine.backtest.commission_rate > 1.0
239 {
240 return Err(VeloraError::ConfigError(
241 "Commission rate must be between 0 and 1".to_string(),
242 ));
243 }
244
245 // Validate risk config
246 if self.risk.max_drawdown_percent <= 0.0 || self.risk.max_drawdown_percent > 100.0 {
247 return Err(VeloraError::ConfigError(
248 "Max drawdown must be between 0 and 100".to_string(),
249 ));
250 }
251
252 Ok(())
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use std::env;
260
261 #[test]
262 fn test_default_config() {
263 let config = VeloraConfig::default();
264 assert_eq!(config.engine.backtest.initial_capital, 10000.0);
265 assert_eq!(config.risk.max_drawdown_percent, 20.0);
266 assert!(config.engine.live.dry_run);
267 assert_eq!(config.database.backend, DatabaseBackend::InMemory);
268 }
269
270 #[test]
271 fn test_config_validation() {
272 let mut config = VeloraConfig::default();
273 assert!(config.validate().is_ok());
274
275 // Test invalid capital
276 config.engine.backtest.initial_capital = -100.0;
277 assert!(config.validate().is_err());
278
279 // Reset and test invalid commission
280 config.engine.backtest.initial_capital = 10000.0;
281 config.engine.backtest.commission_rate = 1.5;
282 assert!(config.validate().is_err());
283
284 // Reset and test invalid drawdown
285 config.engine.backtest.commission_rate = 0.001;
286 config.risk.max_drawdown_percent = 150.0;
287 assert!(config.validate().is_err());
288 }
289
290 #[test]
291 fn test_env_override() {
292 // Set environment variables
293 env::set_var("VELORA_ENGINE_LIVE_DRY_RUN", "false");
294 env::set_var("VELORA_ENGINE_BACKTEST_INITIAL_CAPITAL", "50000");
295 env::set_var("VELORA_RISK_MAX_POSITION_SIZE", "5000");
296
297 // Note: gonfig loads from env automatically with from_gonfig()
298 // For now, just test that defaults work
299 let config = VeloraConfig::default();
300 assert!(config.validate().is_ok());
301
302 // Clean up
303 env::remove_var("VELORA_ENGINE_LIVE_DRY_RUN");
304 env::remove_var("VELORA_ENGINE_BACKTEST_INITIAL_CAPITAL");
305 env::remove_var("VELORA_RISK_MAX_POSITION_SIZE");
306 }
307
308 #[test]
309 fn test_database_backends() {
310 let config = VeloraConfig::default();
311 assert_eq!(config.database.backend, DatabaseBackend::InMemory);
312
313 let questdb_config = QuestDbConfig::default();
314 assert_eq!(questdb_config.pg_port, 8812);
315 assert_eq!(questdb_config.database, "qdb");
316
317 let timescale_config = TimescaleDbConfig::default();
318 assert_eq!(timescale_config.port, 5432);
319 assert_eq!(timescale_config.database, "velora");
320 }
321
322 #[test]
323 fn test_rate_limit_default() {
324 let rate_limit = RateLimitConfig::default();
325 assert_eq!(rate_limit.requests_per_second, 10);
326 assert_eq!(rate_limit.orders_per_second, 5);
327 }
328
329 #[test]
330 fn test_config_serialization() {
331 let config = VeloraConfig::default();
332 let json = serde_json::to_string(&config).unwrap();
333 assert!(json.contains("exchanges"));
334 assert!(json.contains("engine"));
335 assert!(json.contains("database"));
336 }
337}