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/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(&current_weekday) {
275
0
            return false;
276
0
        }
277
278
        // Check if it's a holiday
279
0
        if self.holidays.contains(&current_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
}