/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs
Line | Count | Source |
1 | | //! Symbol classification and configuration management for trading instruments. |
2 | | //! |
3 | | //! This module provides comprehensive symbol classification and configuration |
4 | | //! management for various financial instruments in the Foxhunt HFT trading system. |
5 | | //! It handles asset classification, volatility profiles, trading hours, and |
6 | | //! market-specific parameters for optimal trading execution. |
7 | | |
8 | | use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc, Weekday}; |
9 | | use serde::{Deserialize, Serialize}; |
10 | | use std::collections::HashMap; |
11 | | use std::time::Duration; |
12 | | use uuid::Uuid; |
13 | | |
14 | | /// Asset classification enumeration for different financial instrument types. |
15 | | /// |
16 | | /// Provides standardized classification for all tradeable instruments, |
17 | | /// enabling type-specific risk management, execution logic, and regulatory |
18 | | /// compliance across different asset classes. |
19 | | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
20 | | pub enum AssetClassification { |
21 | | /// Equity securities (stocks, ADRs, REITs) |
22 | | Equity, |
23 | | /// Futures contracts (commodities, financials, indices) |
24 | | Future, |
25 | | /// Foreign exchange pairs (major, minor, exotic) |
26 | | Forex, |
27 | | /// Cryptocurrency and digital assets |
28 | | Crypto, |
29 | | /// Physical commodities (metals, energy, agriculture) |
30 | | Commodity, |
31 | | /// Fixed income securities (bonds, notes, bills) |
32 | | FixedIncome, |
33 | | /// Options contracts (equity, index, commodity options) |
34 | | Option, |
35 | | /// Exchange-traded funds and products |
36 | | Etf, |
37 | | /// Indices and benchmark instruments |
38 | | Index, |
39 | | /// Structured products and derivatives |
40 | | Derivative, |
41 | | } |
42 | | |
43 | | impl AssetClassification { |
44 | | /// Returns the regulatory classification for compliance purposes. |
45 | 3 | pub fn regulatory_class(&self) -> &'static str { |
46 | 3 | match self { |
47 | 1 | AssetClassification::Equity => "EQUITY", |
48 | 0 | AssetClassification::Future => "FUTURE", |
49 | 1 | AssetClassification::Forex => "FX", |
50 | 1 | AssetClassification::Crypto => "CRYPTO", |
51 | 0 | AssetClassification::Commodity => "COMMODITY", |
52 | 0 | AssetClassification::FixedIncome => "FIXED_INCOME", |
53 | 0 | AssetClassification::Option => "OPTION", |
54 | 0 | AssetClassification::Etf => "ETF", |
55 | 0 | AssetClassification::Index => "INDEX", |
56 | 0 | AssetClassification::Derivative => "DERIVATIVE", |
57 | | } |
58 | 3 | } |
59 | | |
60 | | /// Returns whether this asset class requires T+1 settlement. |
61 | 0 | pub fn requires_t_plus_one_settlement(&self) -> bool { |
62 | 0 | matches!(self, AssetClassification::Equity | AssetClassification::Etf) |
63 | 0 | } |
64 | | |
65 | | /// Returns whether this asset class supports after-hours trading. |
66 | 0 | pub fn supports_extended_hours(&self) -> bool { |
67 | 0 | matches!( |
68 | 0 | self, |
69 | | AssetClassification::Equity |
70 | | | AssetClassification::Etf |
71 | | | AssetClassification::Forex |
72 | | | AssetClassification::Crypto |
73 | | ) |
74 | 0 | } |
75 | | } |
76 | | |
77 | | /// Volatility profile configuration for risk management and position sizing. |
78 | | /// |
79 | | /// Defines volatility characteristics and risk parameters for different |
80 | | /// instruments, enabling dynamic position sizing and risk-adjusted execution. |
81 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
82 | | pub struct VolatilityProfile { |
83 | | /// Historical average volatility (annualized) |
84 | | pub average_volatility: f64, |
85 | | /// Maximum observed volatility (99th percentile) |
86 | | pub max_volatility: f64, |
87 | | /// Minimum observed volatility (1st percentile) |
88 | | pub min_volatility: f64, |
89 | | /// Beta coefficient relative to market index |
90 | | pub beta: f64, |
91 | | /// Average True Range (ATR) for recent period |
92 | | pub atr: f64, |
93 | | /// Correlation with market benchmark |
94 | | pub market_correlation: f64, |
95 | | /// Volatility regime classification |
96 | | pub volatility_regime: VolatilityRegime, |
97 | | /// Last updated timestamp for volatility metrics |
98 | | pub last_updated: DateTime<Utc>, |
99 | | /// Number of observations used for calculation |
100 | | pub sample_size: u32, |
101 | | } |
102 | | |
103 | | impl VolatilityProfile { |
104 | | /// Creates a new volatility profile with default values. |
105 | 3 | pub fn new() -> Self { |
106 | 3 | Self { |
107 | 3 | average_volatility: 0.20, |
108 | 3 | max_volatility: 1.00, |
109 | 3 | min_volatility: 0.05, |
110 | 3 | beta: 1.0, |
111 | 3 | atr: 0.0, |
112 | 3 | market_correlation: 0.0, |
113 | 3 | volatility_regime: VolatilityRegime::Normal, |
114 | 3 | last_updated: Utc::now(), |
115 | 3 | sample_size: 0, |
116 | 3 | } |
117 | 3 | } |
118 | | |
119 | | /// Updates volatility metrics with new data point. |
120 | 1 | pub fn update_metrics(&mut self, new_volatility: f64, new_atr: f64) { |
121 | | // Update exponential moving average |
122 | 1 | let alpha = 0.1; // Smoothing factor |
123 | 1 | self.average_volatility = alpha * new_volatility + (1.0 - alpha) * self.average_volatility; |
124 | 1 | self.atr = alpha * new_atr + (1.0 - alpha) * self.atr; |
125 | 1 | self.last_updated = Utc::now(); |
126 | 1 | self.sample_size += 1; |
127 | | |
128 | | // Update volatility regime |
129 | 1 | self.volatility_regime = self.classify_regime(); |
130 | 1 | } |
131 | | |
132 | | /// Classifies current volatility regime based on metrics. |
133 | 1 | fn classify_regime(&self) -> VolatilityRegime { |
134 | 1 | let volatility_ratio = self.average_volatility / 0.20; // Relative to 20% baseline |
135 | | |
136 | 1 | if volatility_ratio > 2.0 { |
137 | 0 | VolatilityRegime::High |
138 | 1 | } else if volatility_ratio > 1.5 { |
139 | 0 | VolatilityRegime::Elevated |
140 | 1 | } else if volatility_ratio < 0.5 { |
141 | 0 | VolatilityRegime::Low |
142 | | } else { |
143 | 1 | VolatilityRegime::Normal |
144 | | } |
145 | 1 | } |
146 | | |
147 | | /// Returns risk-adjusted position size multiplier. |
148 | 0 | pub fn position_size_multiplier(&self) -> f64 { |
149 | 0 | match self.volatility_regime { |
150 | 0 | VolatilityRegime::Low => 1.5, |
151 | 0 | VolatilityRegime::Normal => 1.0, |
152 | 0 | VolatilityRegime::Elevated => 0.7, |
153 | 0 | VolatilityRegime::High => 0.4, |
154 | | } |
155 | 0 | } |
156 | | } |
157 | | |
158 | | impl Default for VolatilityProfile { |
159 | 0 | fn default() -> Self { |
160 | 0 | Self::new() |
161 | 0 | } |
162 | | } |
163 | | |
164 | | /// Volatility regime classification for risk management. |
165 | | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
166 | | pub enum VolatilityRegime { |
167 | | /// Low volatility environment (< 50% of normal) |
168 | | Low, |
169 | | /// Normal volatility environment |
170 | | Normal, |
171 | | /// Elevated volatility (50-100% above normal) |
172 | | Elevated, |
173 | | /// High volatility environment (> 100% above normal) |
174 | | High, |
175 | | } |
176 | | |
177 | | /// Trading hours configuration for different markets and sessions. |
178 | | /// |
179 | | /// Defines market operating hours, pre-market and after-hours sessions, |
180 | | /// and holiday schedules for accurate trade timing and execution. |
181 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
182 | | pub struct TradingHours { |
183 | | /// Primary market timezone identifier (e.g., "America/New_York") |
184 | | pub timezone: String, |
185 | | /// Regular trading session start time |
186 | | pub market_open: NaiveTime, |
187 | | /// Regular trading session end time |
188 | | pub market_close: NaiveTime, |
189 | | /// Pre-market session start time (optional) |
190 | | pub pre_market_open: Option<NaiveTime>, |
191 | | /// After-hours session end time (optional) |
192 | | pub after_hours_close: Option<NaiveTime>, |
193 | | /// Trading days of the week |
194 | | pub trading_days: Vec<Weekday>, |
195 | | /// Market holidays (dates when market is closed) |
196 | | pub holidays: Vec<NaiveDate>, |
197 | | /// Half-day sessions with early close times |
198 | | pub half_days: HashMap<NaiveDate, NaiveTime>, |
199 | | } |
200 | | |
201 | | impl TradingHours { |
202 | | /// Creates US equity market trading hours configuration. |
203 | 3 | pub fn us_equity() -> Self { |
204 | 3 | Self { |
205 | 3 | timezone: "America/New_York".to_string(), |
206 | 3 | market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(), |
207 | 3 | market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(), |
208 | 3 | pre_market_open: Some(NaiveTime::from_hms_opt(4, 0, 0).unwrap()), |
209 | 3 | after_hours_close: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()), |
210 | 3 | trading_days: vec![ |
211 | 3 | Weekday::Mon, |
212 | 3 | Weekday::Tue, |
213 | 3 | Weekday::Wed, |
214 | 3 | Weekday::Thu, |
215 | 3 | Weekday::Fri, |
216 | 3 | ], |
217 | 3 | holidays: vec![], |
218 | 3 | half_days: HashMap::new(), |
219 | 3 | } |
220 | 3 | } |
221 | | |
222 | | /// Creates 24/7 trading hours for crypto markets. |
223 | 0 | pub fn crypto_24_7() -> Self { |
224 | 0 | Self { |
225 | 0 | timezone: "UTC".to_string(), |
226 | 0 | market_open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(), |
227 | 0 | market_close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(), |
228 | 0 | pre_market_open: None, |
229 | 0 | after_hours_close: None, |
230 | 0 | trading_days: vec![ |
231 | 0 | Weekday::Mon, |
232 | 0 | Weekday::Tue, |
233 | 0 | Weekday::Wed, |
234 | 0 | Weekday::Thu, |
235 | 0 | Weekday::Fri, |
236 | 0 | Weekday::Sat, |
237 | 0 | Weekday::Sun, |
238 | 0 | ], |
239 | 0 | holidays: vec![], |
240 | 0 | half_days: HashMap::new(), |
241 | 0 | } |
242 | 0 | } |
243 | | |
244 | | /// Creates forex market trading hours (Sunday 5 PM to Friday 5 PM EST). |
245 | 0 | pub fn forex() -> Self { |
246 | 0 | Self { |
247 | 0 | timezone: "America/New_York".to_string(), |
248 | 0 | market_open: NaiveTime::from_hms_opt(17, 0, 0).unwrap(), |
249 | 0 | market_close: NaiveTime::from_hms_opt(17, 0, 0).unwrap(), |
250 | 0 | pre_market_open: None, |
251 | 0 | after_hours_close: None, |
252 | 0 | trading_days: vec![ |
253 | 0 | Weekday::Sun, |
254 | 0 | Weekday::Mon, |
255 | 0 | Weekday::Tue, |
256 | 0 | Weekday::Wed, |
257 | 0 | Weekday::Thu, |
258 | 0 | Weekday::Fri, |
259 | 0 | ], |
260 | 0 | holidays: vec![], |
261 | 0 | half_days: HashMap::new(), |
262 | 0 | } |
263 | 0 | } |
264 | | |
265 | | /// Checks if market is currently open. |
266 | 0 | pub fn is_market_open(&self, current_time: DateTime<Utc>) -> bool { |
267 | | // Convert to market timezone and check if within trading hours |
268 | | // This is a simplified implementation - production would use proper timezone handling |
269 | 0 | let current_date = current_time.date_naive(); |
270 | 0 | let current_time = current_time.time(); |
271 | 0 | let current_weekday = current_date.weekday(); |
272 | | |
273 | | // Check if it's a trading day |
274 | 0 | if !self.trading_days.contains(¤t_weekday) { |
275 | 0 | return false; |
276 | 0 | } |
277 | | |
278 | | // Check if it's a holiday |
279 | 0 | if self.holidays.contains(¤t_date) { |
280 | 0 | return false; |
281 | 0 | } |
282 | | |
283 | | // Check if within trading hours |
284 | 0 | current_time >= self.market_open && current_time <= self.market_close |
285 | 0 | } |
286 | | |
287 | | /// Checks if extended hours trading is active. |
288 | 0 | pub fn is_extended_hours_open(&self, current_time: DateTime<Utc>) -> bool { |
289 | 0 | let current_time = current_time.time(); |
290 | | |
291 | | // Check pre-market |
292 | 0 | if let Some(pre_open) = self.pre_market_open { |
293 | 0 | if current_time >= pre_open && current_time < self.market_open { |
294 | 0 | return true; |
295 | 0 | } |
296 | 0 | } |
297 | | |
298 | | // Check after-hours |
299 | 0 | if let Some(after_close) = self.after_hours_close { |
300 | 0 | if current_time > self.market_close && current_time <= after_close { |
301 | 0 | return true; |
302 | 0 | } |
303 | 0 | } |
304 | | |
305 | 0 | false |
306 | 0 | } |
307 | | } |
308 | | |
309 | | impl Default for TradingHours { |
310 | 0 | fn default() -> Self { |
311 | 0 | Self::us_equity() |
312 | 0 | } |
313 | | } |
314 | | |
315 | | /// Comprehensive symbol configuration containing all trading parameters. |
316 | | /// |
317 | | /// Central configuration structure for each tradeable symbol, containing |
318 | | /// classification, market parameters, risk settings, and execution rules. |
319 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
320 | | pub struct SymbolConfig { |
321 | | /// Unique symbol identifier |
322 | | pub symbol: String, |
323 | | /// Symbol description or company name |
324 | | pub description: String, |
325 | | /// Asset classification |
326 | | pub classification: AssetClassification, |
327 | | /// Volatility and risk profile |
328 | | pub volatility_profile: VolatilityProfile, |
329 | | /// Market operating hours |
330 | | pub trading_hours: TradingHours, |
331 | | /// Minimum price increment (tick size) |
332 | | pub tick_size: f64, |
333 | | /// Standard trading unit size |
334 | | pub lot_size: f64, |
335 | | /// Minimum order quantity |
336 | | pub min_order_size: f64, |
337 | | /// Maximum order quantity |
338 | | pub max_order_size: f64, |
339 | | /// Primary exchange or venue |
340 | | pub primary_exchange: String, |
341 | | /// Currency denomination |
342 | | pub currency: String, |
343 | | /// Sector classification (for equities) |
344 | | pub sector: Option<String>, |
345 | | /// Industry classification (for equities) |
346 | | pub industry: Option<String>, |
347 | | /// Market capitalization (for equities) |
348 | | pub market_cap: Option<f64>, |
349 | | /// Average daily volume |
350 | | pub avg_daily_volume: f64, |
351 | | /// Margin requirements |
352 | | pub margin_requirement: f64, |
353 | | /// Position limits |
354 | | pub position_limit: Option<f64>, |
355 | | /// Risk multiplier for position sizing |
356 | | pub risk_multiplier: f64, |
357 | | /// Configuration metadata |
358 | | pub metadata: SymbolMetadata, |
359 | | } |
360 | | |
361 | | impl SymbolConfig { |
362 | | /// Creates a new symbol configuration with default values. |
363 | 2 | pub fn new(symbol: String, classification: AssetClassification) -> Self { |
364 | 2 | let trading_hours = match classification { |
365 | 0 | AssetClassification::Crypto => TradingHours::crypto_24_7(), |
366 | 0 | AssetClassification::Forex => TradingHours::forex(), |
367 | 2 | _ => TradingHours::us_equity(), |
368 | | }; |
369 | | |
370 | 2 | Self { |
371 | 2 | symbol: symbol.clone(), |
372 | 2 | description: format!("{} - Auto-generated", symbol), |
373 | 2 | classification, |
374 | 2 | volatility_profile: VolatilityProfile::new(), |
375 | 2 | trading_hours, |
376 | 2 | tick_size: 0.01, |
377 | 2 | lot_size: 1.0, |
378 | 2 | min_order_size: 1.0, |
379 | 2 | max_order_size: 1_000_000.0, |
380 | 2 | primary_exchange: "".to_string(), |
381 | 2 | currency: "USD".to_string(), |
382 | 2 | sector: None, |
383 | 2 | industry: None, |
384 | 2 | market_cap: None, |
385 | 2 | avg_daily_volume: 0.0, |
386 | 2 | margin_requirement: 0.25, |
387 | 2 | position_limit: None, |
388 | 2 | risk_multiplier: 1.0, |
389 | 2 | metadata: SymbolMetadata::new(), |
390 | 2 | } |
391 | 2 | } |
392 | | |
393 | | /// Validates the symbol configuration for correctness. |
394 | 3 | pub fn validate(&self) -> Result<(), String> { |
395 | 3 | if self.symbol.is_empty() { |
396 | 0 | return Err("Symbol cannot be empty".to_string()); |
397 | 3 | } |
398 | | |
399 | 3 | if self.tick_size <= 0.0 { |
400 | 1 | return Err("Tick size must be positive".to_string()); |
401 | 2 | } |
402 | | |
403 | 2 | if self.lot_size <= 0.0 { |
404 | 0 | return Err("Lot size must be positive".to_string()); |
405 | 2 | } |
406 | | |
407 | 2 | if self.min_order_size <= 0.0 { |
408 | 0 | return Err("Minimum order size must be positive".to_string()); |
409 | 2 | } |
410 | | |
411 | 2 | if self.max_order_size <= self.min_order_size { |
412 | 0 | return Err("Maximum order size must be greater than minimum".to_string()); |
413 | 2 | } |
414 | | |
415 | 2 | if self.margin_requirement < 0.0 || self.margin_requirement > 1.0 { |
416 | 0 | return Err("Margin requirement must be between 0 and 1".to_string()); |
417 | 2 | } |
418 | | |
419 | 2 | Ok(()) |
420 | 3 | } |
421 | | |
422 | | /// Calculates the effective position size based on risk parameters. |
423 | 0 | pub fn calculate_position_size(&self, base_size: f64, _account_value: f64) -> f64 { |
424 | 0 | let volatility_multiplier = self.volatility_profile.position_size_multiplier(); |
425 | 0 | let risk_adjusted_size = base_size * volatility_multiplier * self.risk_multiplier; |
426 | | |
427 | | // Apply position limits |
428 | 0 | if let Some(limit) = self.position_limit { |
429 | 0 | risk_adjusted_size.min(limit) |
430 | | } else { |
431 | 0 | risk_adjusted_size |
432 | | } |
433 | 0 | } |
434 | | |
435 | | /// Returns the appropriate tick size for a given price level. |
436 | 0 | pub fn get_tick_size_for_price(&self, _price: f64) -> f64 { |
437 | | // Some markets have variable tick sizes based on price |
438 | | // This is a simplified implementation |
439 | 0 | self.tick_size |
440 | 0 | } |
441 | | |
442 | | /// Rounds price to the nearest valid tick. |
443 | 0 | pub fn round_to_tick(&self, price: f64) -> f64 { |
444 | 0 | let tick = self.get_tick_size_for_price(price); |
445 | 0 | (price / tick).round() * tick |
446 | 0 | } |
447 | | |
448 | | /// Checks if the symbol is currently tradeable. |
449 | 0 | pub fn is_tradeable(&self, current_time: DateTime<Utc>) -> bool { |
450 | 0 | self.trading_hours.is_market_open(current_time) && self.metadata.is_active |
451 | 0 | } |
452 | | |
453 | | /// Checks if extended hours trading is available. |
454 | 0 | pub fn supports_extended_hours(&self) -> bool { |
455 | 0 | self.classification.supports_extended_hours() |
456 | 0 | } |
457 | | } |
458 | | |
459 | | /// Symbol configuration metadata for versioning and tracking. |
460 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
461 | | pub struct SymbolMetadata { |
462 | | /// Unique configuration ID |
463 | | pub id: Uuid, |
464 | | /// Configuration version |
465 | | pub version: u32, |
466 | | /// Creation timestamp |
467 | | pub created_at: DateTime<Utc>, |
468 | | /// Last update timestamp |
469 | | pub updated_at: DateTime<Utc>, |
470 | | /// Active status |
471 | | pub is_active: bool, |
472 | | /// Data source for configuration |
473 | | pub data_source: String, |
474 | | /// Last validation timestamp |
475 | | pub last_validated: Option<DateTime<Utc>>, |
476 | | /// Configuration tags for organization |
477 | | pub tags: Vec<String>, |
478 | | } |
479 | | |
480 | | impl SymbolMetadata { |
481 | | /// Creates new metadata with default values. |
482 | 2 | pub fn new() -> Self { |
483 | 2 | let now = Utc::now(); |
484 | 2 | Self { |
485 | 2 | id: Uuid::new_v4(), |
486 | 2 | version: 1, |
487 | 2 | created_at: now, |
488 | 2 | updated_at: now, |
489 | 2 | is_active: true, |
490 | 2 | data_source: "manual".to_string(), |
491 | 2 | last_validated: None, |
492 | 2 | tags: vec![], |
493 | 2 | } |
494 | 2 | } |
495 | | |
496 | | /// Updates the metadata timestamp and version. |
497 | 0 | pub fn update(&mut self) { |
498 | 0 | self.updated_at = Utc::now(); |
499 | 0 | self.version += 1; |
500 | 0 | } |
501 | | |
502 | | /// Marks the configuration as validated. |
503 | 0 | pub fn mark_validated(&mut self) { |
504 | 0 | self.last_validated = Some(Utc::now()); |
505 | 0 | } |
506 | | } |
507 | | |
508 | | impl Default for SymbolMetadata { |
509 | 0 | fn default() -> Self { |
510 | 0 | Self::new() |
511 | 0 | } |
512 | | } |
513 | | |
514 | | /// Symbol configuration manager for loading and caching symbol configurations. |
515 | | /// |
516 | | /// Provides high-performance access to symbol configurations with caching, |
517 | | /// hot-reload capabilities, and configuration validation. |
518 | | #[derive(Debug)] |
519 | | pub struct SymbolConfigManager { |
520 | | /// In-memory cache of symbol configurations |
521 | | symbol_cache: HashMap<String, SymbolConfig>, |
522 | | /// Last cache update timestamp |
523 | | last_updated: DateTime<Utc>, |
524 | | /// Cache timeout duration |
525 | | cache_timeout: Duration, |
526 | | } |
527 | | |
528 | | impl SymbolConfigManager { |
529 | | /// Creates a new symbol configuration manager. |
530 | 1 | pub fn new() -> Self { |
531 | 1 | Self { |
532 | 1 | symbol_cache: HashMap::new(), |
533 | 1 | last_updated: Utc::now(), |
534 | 1 | cache_timeout: Duration::from_secs(300), // 5 minutes |
535 | 1 | } |
536 | 1 | } |
537 | | |
538 | | /// Loads symbol configuration from cache or source. |
539 | 0 | pub async fn get_symbol_config( |
540 | 0 | &mut self, |
541 | 0 | symbol: &str, |
542 | 0 | ) -> Result<Option<SymbolConfig>, String> { |
543 | | // Check cache first |
544 | 0 | if let Some(config) = self.symbol_cache.get(symbol) { |
545 | 0 | if !self.is_cache_expired() { |
546 | 0 | return Ok(Some(config.clone())); |
547 | 0 | } |
548 | 0 | } |
549 | | |
550 | | // Load from source (this would integrate with database/external source) |
551 | 0 | self.load_symbol_from_source(symbol).await |
552 | 0 | } |
553 | | |
554 | | /// Loads all symbol configurations into cache. |
555 | 0 | pub async fn load_all_symbols(&mut self) -> Result<usize, String> { |
556 | | // This would integrate with the database or external configuration source |
557 | 0 | self.refresh_cache().await |
558 | 0 | } |
559 | | |
560 | | /// Adds or updates a symbol configuration. |
561 | 1 | pub fn upsert_symbol_config(&mut self, config: SymbolConfig) -> Result<(), String> { |
562 | | // Validate configuration |
563 | 1 | config.validate()?0 ; |
564 | | |
565 | | // Update cache |
566 | 1 | self.symbol_cache.insert(config.symbol.clone(), config); |
567 | 1 | self.last_updated = Utc::now(); |
568 | | |
569 | 1 | Ok(()) |
570 | 1 | } |
571 | | |
572 | | /// Removes a symbol configuration. |
573 | 0 | pub fn remove_symbol_config(&mut self, symbol: &str) -> Option<SymbolConfig> { |
574 | 0 | self.symbol_cache.remove(symbol) |
575 | 0 | } |
576 | | |
577 | | /// Returns all cached symbol configurations. |
578 | 1 | pub fn get_all_symbols(&self) -> Vec<&SymbolConfig> { |
579 | 1 | self.symbol_cache.values().collect() |
580 | 1 | } |
581 | | |
582 | | /// Returns symbols filtered by asset classification. |
583 | 0 | pub fn get_symbols_by_classification( |
584 | 0 | &self, |
585 | 0 | classification: &AssetClassification, |
586 | 0 | ) -> Vec<&SymbolConfig> { |
587 | 0 | self.symbol_cache |
588 | 0 | .values() |
589 | 0 | .filter(|config| &config.classification == classification) |
590 | 0 | .collect() |
591 | 0 | } |
592 | | |
593 | | /// Checks if cache has expired. |
594 | 0 | fn is_cache_expired(&self) -> bool { |
595 | 0 | Utc::now() |
596 | 0 | .signed_duration_since(self.last_updated) |
597 | 0 | .to_std() |
598 | 0 | .unwrap_or(Duration::MAX) |
599 | 0 | > self.cache_timeout |
600 | 0 | } |
601 | | |
602 | | /// Loads symbol configuration from external source. |
603 | 0 | async fn load_symbol_from_source( |
604 | 0 | &mut self, |
605 | 0 | _symbol: &str, |
606 | 0 | ) -> Result<Option<SymbolConfig>, String> { |
607 | | // This would integrate with database or external configuration API |
608 | | // For now, return None to indicate symbol not found |
609 | | |
610 | | // Example of creating a default config if needed: |
611 | | // let config = SymbolConfig::new(symbol.to_string(), AssetClassification::Equity); |
612 | | // self.symbol_cache.insert(symbol.to_string(), config.clone()); |
613 | | // Ok(Some(config)) |
614 | | |
615 | 0 | Ok(None) |
616 | 0 | } |
617 | | |
618 | | /// Refreshes the entire symbol cache from source. |
619 | 0 | async fn refresh_cache(&mut self) -> Result<usize, String> { |
620 | | // This would integrate with database to load all active symbols |
621 | | // For now, return the current cache size |
622 | 0 | Ok(self.symbol_cache.len()) |
623 | 0 | } |
624 | | |
625 | | /// Sets cache timeout duration. |
626 | 0 | pub fn set_cache_timeout(&mut self, timeout: Duration) { |
627 | 0 | self.cache_timeout = timeout; |
628 | 0 | } |
629 | | |
630 | | /// Forces cache refresh on next access. |
631 | 0 | pub fn invalidate_cache(&mut self) { |
632 | 0 | self.last_updated = DateTime::<Utc>::MIN_UTC; |
633 | 0 | } |
634 | | |
635 | | /// Returns cache statistics. |
636 | 0 | pub fn cache_stats(&self) -> (usize, DateTime<Utc>, bool) { |
637 | 0 | ( |
638 | 0 | self.symbol_cache.len(), |
639 | 0 | self.last_updated, |
640 | 0 | self.is_cache_expired(), |
641 | 0 | ) |
642 | 0 | } |
643 | | } |
644 | | |
645 | | impl Default for SymbolConfigManager { |
646 | 0 | fn default() -> Self { |
647 | 0 | Self::new() |
648 | 0 | } |
649 | | } |
650 | | |
651 | | #[cfg(test)] |
652 | | mod tests { |
653 | | use super::*; |
654 | | |
655 | | #[test] |
656 | 1 | fn test_asset_classification_regulatory_class() { |
657 | 1 | assert_eq!(AssetClassification::Equity.regulatory_class(), "EQUITY"); |
658 | 1 | assert_eq!(AssetClassification::Forex.regulatory_class(), "FX"); |
659 | 1 | assert_eq!(AssetClassification::Crypto.regulatory_class(), "CRYPTO"); |
660 | 1 | } |
661 | | |
662 | | #[test] |
663 | 1 | fn test_volatility_profile_update() { |
664 | 1 | let mut profile = VolatilityProfile::new(); |
665 | 1 | profile.update_metrics(0.40, 2.5); |
666 | | |
667 | | // With exponential smoothing: 0.1 * 0.40 + 0.9 * 0.20 = 0.22 |
668 | 1 | assert!(profile.average_volatility > 0.20 && profile.average_volatility < 0.25); |
669 | | // With exponential smoothing: 0.1 * 2.5 + 0.9 * 0.0 = 0.25 |
670 | 1 | assert!((profile.atr - 0.25).abs() < 0.01); |
671 | 1 | assert_eq!(profile.volatility_regime, VolatilityRegime::Normal); |
672 | 1 | } |
673 | | |
674 | | #[test] |
675 | 1 | fn test_symbol_config_validation() { |
676 | 1 | let mut config = SymbolConfig::new("AAPL".to_string(), AssetClassification::Equity); |
677 | 1 | assert!(config.validate().is_ok()); |
678 | | |
679 | 1 | config.tick_size = -0.01; |
680 | 1 | assert!(config.validate().is_err()); |
681 | 1 | } |
682 | | |
683 | | #[test] |
684 | 1 | fn test_trading_hours_us_equity() { |
685 | 1 | let hours = TradingHours::us_equity(); |
686 | 1 | assert_eq!(hours.timezone, "America/New_York"); |
687 | 1 | assert_eq!( |
688 | | hours.market_open, |
689 | 1 | NaiveTime::from_hms_opt(9, 30, 0).unwrap() |
690 | | ); |
691 | 1 | assert_eq!( |
692 | | hours.market_close, |
693 | 1 | NaiveTime::from_hms_opt(16, 0, 0).unwrap() |
694 | | ); |
695 | 1 | } |
696 | | |
697 | | #[test] |
698 | 1 | fn test_symbol_config_manager() { |
699 | 1 | let mut manager = SymbolConfigManager::new(); |
700 | 1 | let config = SymbolConfig::new("TEST".to_string(), AssetClassification::Equity); |
701 | | |
702 | 1 | assert!(manager.upsert_symbol_config(config).is_ok()); |
703 | 1 | assert_eq!(manager.get_all_symbols().len(), 1); |
704 | 1 | } |
705 | | } |