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/data_providers.rs
Line
Count
Source
1
//! Data provider endpoint configuration
2
//!
3
//! Centralizes all hardcoded API endpoints for data providers, enabling
4
//! environment-specific configurations and easy switching between dev/staging/prod.
5
6
use serde::{Deserialize, Serialize};
7
8
/// Environment specification for data providers
9
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10
pub enum DataProviderEnvironment {
11
    /// Development environment with potentially mocked or sandbox endpoints
12
    Development,
13
    /// Staging environment for pre-production testing
14
    Staging,
15
    /// Production environment with live data
16
    Production,
17
}
18
19
impl DataProviderEnvironment {
20
    /// Detect environment from FOXHUNT_ENV environment variable
21
4
    pub fn from_env() -> Self {
22
4
        match std::env::var("FOXHUNT_ENV")
23
4
            .unwrap_or_else(|_| 
"development"1
.
to_string1
())
24
4
            .to_lowercase()
25
4
            .as_str()
26
        {
27
4
            "prod" | "production" => 
Self::Production1
,
28
3
            "staging" | 
"stage"2
=>
Self::Staging1
,
29
2
            _ => Self::Development,
30
        }
31
4
    }
32
}
33
34
/// Databento endpoint configuration
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct DatabentoEndpoints {
37
    /// WebSocket URL for real-time data streaming
38
    pub websocket_url: String,
39
    /// HTTP base URL for historical data queries
40
    pub historical_base_url: String,
41
}
42
43
impl DatabentoEndpoints {
44
    /// Create configuration from environment variables with fallback to defaults
45
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
46
3
        let (ws_default, http_default) = match environment {
47
3
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
48
3
                "wss://gateway.databento.com/v0/subscribe",
49
3
                "https://hist.databento.com",
50
3
            ),
51
0
            DataProviderEnvironment::Staging => (
52
0
                "wss://staging-gateway.databento.com/v0/subscribe",
53
0
                "https://staging-hist.databento.com",
54
0
            ),
55
        };
56
57
        Self {
58
3
            websocket_url: std::env::var("DATABENTO_WS_URL")
59
3
                .unwrap_or_else(|_| 
ws_default2
.
to_string2
()),
60
3
            historical_base_url: std::env::var("DATABENTO_HTTP_URL")
61
3
                .unwrap_or_else(|_| http_default.to_string()),
62
        }
63
3
    }
64
}
65
66
impl Default for DatabentoEndpoints {
67
0
    fn default() -> Self {
68
0
        Self::from_env(DataProviderEnvironment::from_env())
69
0
    }
70
}
71
72
/// Benzinga endpoint configuration
73
#[derive(Debug, Clone, Serialize, Deserialize)]
74
pub struct BenzingaEndpoints {
75
    /// WebSocket URL for real-time news and sentiment streaming
76
    pub websocket_url: String,
77
    /// HTTP base URL for API queries
78
    pub api_base_url: String,
79
}
80
81
impl BenzingaEndpoints {
82
    /// Create configuration from environment variables with fallback to defaults
83
2
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
84
2
        let (ws_default, api_default) = match environment {
85
2
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
86
2
                "wss://api.benzinga.com/api/v1/stream",
87
2
                "https://api.benzinga.com/api/v2",
88
2
            ),
89
0
            DataProviderEnvironment::Staging => (
90
0
                "wss://staging-api.benzinga.com/api/v1/stream",
91
0
                "https://staging-api.benzinga.com/api/v2",
92
0
            ),
93
        };
94
95
        Self {
96
2
            websocket_url: std::env::var("BENZINGA_WS_URL")
97
2
                .unwrap_or_else(|_| ws_default.to_string()),
98
2
            api_base_url: std::env::var("BENZINGA_API_URL")
99
2
                .unwrap_or_else(|_| api_default.to_string()),
100
        }
101
2
    }
102
}
103
104
impl Default for BenzingaEndpoints {
105
0
    fn default() -> Self {
106
0
        Self::from_env(DataProviderEnvironment::from_env())
107
0
    }
108
}
109
110
/// Alpaca endpoint configuration
111
#[derive(Debug, Clone, Serialize, Deserialize)]
112
pub struct AlpacaEndpoints {
113
    /// Base URL for trading operations (paper or live)
114
    pub trading_base_url: String,
115
    /// Base URL for market data queries
116
    pub data_base_url: String,
117
}
118
119
impl AlpacaEndpoints {
120
    /// Create configuration from environment variables with fallback to defaults
121
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
122
3
        let (trading_default, data_default) = match environment {
123
1
            DataProviderEnvironment::Development => (
124
1
                "https://paper-api.alpaca.markets",
125
1
                "https://data.alpaca.markets",
126
1
            ),
127
0
            DataProviderEnvironment::Staging => (
128
0
                "https://paper-api.alpaca.markets",
129
0
                "https://data.alpaca.markets",
130
0
            ),
131
2
            DataProviderEnvironment::Production => (
132
2
                "https://api.alpaca.markets",
133
2
                "https://data.alpaca.markets",
134
2
            ),
135
        };
136
137
        Self {
138
3
            trading_base_url: std::env::var("ALPACA_TRADING_URL")
139
3
                .unwrap_or_else(|_| trading_default.to_string()),
140
3
            data_base_url: std::env::var("ALPACA_DATA_URL")
141
3
                .unwrap_or_else(|_| data_default.to_string()),
142
        }
143
3
    }
144
}
145
146
impl Default for AlpacaEndpoints {
147
0
    fn default() -> Self {
148
0
        Self::from_env(DataProviderEnvironment::from_env())
149
0
    }
150
}
151
152
/// Interactive Brokers Gateway configuration
153
#[derive(Debug, Clone, Serialize, Deserialize)]
154
pub struct IBGatewayConfig {
155
    /// Gateway host (typically localhost for local TWS/Gateway)
156
    pub host: String,
157
    /// Gateway port (7497 for paper trading, 7496 for live, 4001 for IB Gateway)
158
    pub port: u16,
159
}
160
161
impl IBGatewayConfig {
162
    /// Create configuration from environment variables with fallback to defaults
163
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
164
3
        let (host_default, port_default) = match environment {
165
1
            DataProviderEnvironment::Development => ("127.0.0.1", 7497), // Paper trading
166
0
            DataProviderEnvironment::Staging => ("127.0.0.1", 7497),     // Paper trading
167
2
            DataProviderEnvironment::Production => ("127.0.0.1", 7496),  // Live trading
168
        };
169
170
        Self {
171
3
            host: std::env::var("IB_GATEWAY_HOST")
172
3
                .unwrap_or_else(|_| host_default.to_string()),
173
3
            port: std::env::var("IB_GATEWAY_PORT")
174
3
                .ok()
175
3
                .and_then(|s| 
s.parse()0
.
ok0
())
176
3
                .unwrap_or(port_default),
177
        }
178
3
    }
179
}
180
181
impl Default for IBGatewayConfig {
182
0
    fn default() -> Self {
183
0
        Self::from_env(DataProviderEnvironment::from_env())
184
0
    }
185
}
186
187
/// Master configuration for all data provider endpoints
188
#[derive(Debug, Clone, Serialize, Deserialize)]
189
pub struct DataProviderConfig {
190
    /// Current environment
191
    pub environment: DataProviderEnvironment,
192
    /// Databento endpoints
193
    pub databento: DatabentoEndpoints,
194
    /// Benzinga endpoints
195
    pub benzinga: BenzingaEndpoints,
196
    /// Alpaca endpoints
197
    pub alpaca: AlpacaEndpoints,
198
    /// Interactive Brokers Gateway configuration
199
    pub ib_gateway: IBGatewayConfig,
200
}
201
202
impl DataProviderConfig {
203
    /// Create configuration from environment
204
0
    pub fn from_env() -> Self {
205
0
        let environment = DataProviderEnvironment::from_env();
206
0
        Self {
207
0
            databento: DatabentoEndpoints::from_env(environment),
208
0
            benzinga: BenzingaEndpoints::from_env(environment),
209
0
            alpaca: AlpacaEndpoints::from_env(environment),
210
0
            ib_gateway: IBGatewayConfig::from_env(environment),
211
0
            environment,
212
0
        }
213
0
    }
214
215
    /// Create configuration for specific environment
216
1
    pub fn for_environment(environment: DataProviderEnvironment) -> Self {
217
1
        Self {
218
1
            databento: DatabentoEndpoints::from_env(environment),
219
1
            benzinga: BenzingaEndpoints::from_env(environment),
220
1
            alpaca: AlpacaEndpoints::from_env(environment),
221
1
            ib_gateway: IBGatewayConfig::from_env(environment),
222
1
            environment,
223
1
        }
224
1
    }
225
}
226
227
impl Default for DataProviderConfig {
228
0
    fn default() -> Self {
229
0
        Self::from_env()
230
0
    }
231
}
232
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
237
    #[test]
238
1
    fn test_environment_detection() {
239
1
        std::env::set_var("FOXHUNT_ENV", "production");
240
1
        assert_eq!(
241
1
            DataProviderEnvironment::from_env(),
242
            DataProviderEnvironment::Production
243
        );
244
245
1
        std::env::set_var("FOXHUNT_ENV", "staging");
246
1
        assert_eq!(
247
1
            DataProviderEnvironment::from_env(),
248
            DataProviderEnvironment::Staging
249
        );
250
251
1
        std::env::set_var("FOXHUNT_ENV", "development");
252
1
        assert_eq!(
253
1
            DataProviderEnvironment::from_env(),
254
            DataProviderEnvironment::Development
255
        );
256
257
1
        std::env::remove_var("FOXHUNT_ENV");
258
1
        assert_eq!(
259
1
            DataProviderEnvironment::from_env(),
260
            DataProviderEnvironment::Development
261
        );
262
1
    }
263
264
    #[test]
265
1
    fn test_databento_defaults() {
266
1
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
267
1
        assert_eq!(
268
            config.websocket_url,
269
            "wss://gateway.databento.com/v0/subscribe"
270
        );
271
1
        assert_eq!(config.historical_base_url, "https://hist.databento.com");
272
1
    }
273
274
    #[test]
275
1
    fn test_benzinga_defaults() {
276
1
        let config = BenzingaEndpoints::from_env(DataProviderEnvironment::Production);
277
1
        assert_eq!(
278
            config.websocket_url,
279
            "wss://api.benzinga.com/api/v1/stream"
280
        );
281
1
        assert_eq!(config.api_base_url, "https://api.benzinga.com/api/v2");
282
1
    }
283
284
    #[test]
285
1
    fn test_alpaca_defaults() {
286
1
        let dev_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Development);
287
1
        assert_eq!(
288
            dev_config.trading_base_url,
289
            "https://paper-api.alpaca.markets"
290
        );
291
292
1
        let prod_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Production);
293
1
        assert_eq!(prod_config.trading_base_url, "https://api.alpaca.markets");
294
1
    }
295
296
    #[test]
297
1
    fn test_ib_gateway_defaults() {
298
1
        let dev_config = IBGatewayConfig::from_env(DataProviderEnvironment::Development);
299
1
        assert_eq!(dev_config.host, "127.0.0.1");
300
1
        assert_eq!(dev_config.port, 7497);
301
302
1
        let prod_config = IBGatewayConfig::from_env(DataProviderEnvironment::Production);
303
1
        assert_eq!(prod_config.port, 7496);
304
1
    }
305
306
    #[test]
307
1
    fn test_environment_variable_override() {
308
1
        std::env::set_var("DATABENTO_WS_URL", "wss://custom.databento.com");
309
1
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
310
1
        assert_eq!(config.websocket_url, "wss://custom.databento.com");
311
1
        std::env::remove_var("DATABENTO_WS_URL");
312
1
    }
313
314
    #[test]
315
1
    fn test_master_config() {
316
1
        let config = DataProviderConfig::for_environment(DataProviderEnvironment::Production);
317
1
        assert_eq!(config.environment, DataProviderEnvironment::Production);
318
1
        assert!(!config.databento.websocket_url.is_empty());
319
1
        assert!(!config.benzinga.websocket_url.is_empty());
320
1
        assert!(!config.alpaca.trading_base_url.is_empty());
321
1
        assert!(!config.ib_gateway.host.is_empty());
322
1
    }
323
}