Skip to main content

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}