- Add #![deny(clippy::unwrap_used, clippy::expect_used)] to config/src/lib.rs
- Add #![deny(clippy::unwrap_used, clippy::expect_used)] to trading_agent_service/src/lib.rs
- Add #![deny(clippy::unwrap_used, clippy::expect_used)] to trading_agent_service/src/main.rs (binary crate)
config crate fixes:
- asset_classification.rs: Replace .parse().unwrap() with Decimal::new() for tick/position sizes
- asset_classification.rs: Replace NaiveTime::from_hms_opt().unwrap() with .unwrap_or_default()
- asset_classification.rs: Add #[allow] on test module
- symbol_config.rs: Add #[allow] on test module (function-level allows already present)
trading_agent_service fixes:
- monitoring.rs: Add #[allow(clippy::expect_used)] on each Lazy static metric registration
- monitoring.rs: Fix start_metrics_server() runtime unwrap/expect calls with safe alternatives
- monitoring.rs: Add #[allow] on test module
- main.rs: Fix health_handler() .unwrap() with .unwrap_or_else() fallback
- main.rs: Fix metrics_handler() .unwrap()/.expect() with let _ / .unwrap_or_default()
- autonomous_scaling.rs: Fix capital parse .expect() with .unwrap_or(0.0)
- autonomous_scaling.rs: Replace .find().cloned().unwrap() with filter_map()
- autonomous_scaling.rs: Replace .find().unwrap() on tier lookup with let-else
- autonomous_scaling.rs: Add #[allow] on test module
- allocation.rs: Fix .unwrap() on Decimal::from_f64_retain(0.20) with .unwrap_or(Decimal::ZERO)
- allocation.rs: Add #[allow] on test module
- orders.rs: Replace BigDecimal::from_str("0").unwrap() with BigDecimal::from(0_i64)
- orders.rs: Add #[allow] on test module
- universe.rs, dynamic_stop_loss.rs, strategies.rs: Add #[allow] on test modules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
737 lines
24 KiB
Rust
737 lines
24 KiB
Rust
//! Symbol classification and configuration management for trading instruments.
|
|
//!
|
|
//! This module provides comprehensive symbol classification and configuration
|
|
//! management for various financial instruments in the Foxhunt HFT trading system.
|
|
//! It handles asset classification, volatility profiles, trading hours, and
|
|
//! market-specific parameters for optimal trading execution.
|
|
|
|
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc, Weekday};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
use uuid::Uuid;
|
|
|
|
/// Asset classification enumeration for different financial instrument types.
|
|
///
|
|
/// Provides standardized classification for all tradeable instruments,
|
|
/// enabling type-specific risk management, execution logic, and regulatory
|
|
/// compliance across different asset classes.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum AssetClassification {
|
|
/// Equity securities (stocks, ADRs, REITs)
|
|
Equity,
|
|
/// Futures contracts (commodities, financials, indices)
|
|
Future,
|
|
/// Foreign exchange pairs (major, minor, exotic)
|
|
Forex,
|
|
/// Cryptocurrency and digital assets
|
|
Crypto,
|
|
/// Physical commodities (metals, energy, agriculture)
|
|
Commodity,
|
|
/// Fixed income securities (bonds, notes, bills)
|
|
FixedIncome,
|
|
/// Options contracts (equity, index, commodity options)
|
|
Option,
|
|
/// Exchange-traded funds and products
|
|
Etf,
|
|
/// Indices and benchmark instruments
|
|
Index,
|
|
/// Structured products and derivatives
|
|
Derivative,
|
|
}
|
|
|
|
impl AssetClassification {
|
|
/// Returns the regulatory classification for compliance purposes.
|
|
pub const fn regulatory_class(&self) -> &'static str {
|
|
match self {
|
|
AssetClassification::Equity => "EQUITY",
|
|
AssetClassification::Future => "FUTURE",
|
|
AssetClassification::Forex => "FX",
|
|
AssetClassification::Crypto => "CRYPTO",
|
|
AssetClassification::Commodity => "COMMODITY",
|
|
AssetClassification::FixedIncome => "FIXED_INCOME",
|
|
AssetClassification::Option => "OPTION",
|
|
AssetClassification::Etf => "ETF",
|
|
AssetClassification::Index => "INDEX",
|
|
AssetClassification::Derivative => "DERIVATIVE",
|
|
}
|
|
}
|
|
|
|
/// Returns whether this asset class requires T+1 settlement.
|
|
pub const fn requires_t_plus_one_settlement(&self) -> bool {
|
|
matches!(self, AssetClassification::Equity | AssetClassification::Etf)
|
|
}
|
|
|
|
/// Returns whether this asset class supports after-hours trading.
|
|
pub const fn supports_extended_hours(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
AssetClassification::Equity
|
|
| AssetClassification::Etf
|
|
| AssetClassification::Forex
|
|
| AssetClassification::Crypto
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Volatility profile configuration for risk management and position sizing.
|
|
///
|
|
/// Defines volatility characteristics and risk parameters for different
|
|
/// instruments, enabling dynamic position sizing and risk-adjusted execution.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VolatilityProfile {
|
|
/// Historical average volatility (annualized)
|
|
pub average_volatility: f64,
|
|
/// Maximum observed volatility (99th percentile)
|
|
pub max_volatility: f64,
|
|
/// Minimum observed volatility (1st percentile)
|
|
pub min_volatility: f64,
|
|
/// Beta coefficient relative to market index
|
|
pub beta: f64,
|
|
/// Average True Range (ATR) for recent period
|
|
pub atr: f64,
|
|
/// Correlation with market benchmark
|
|
pub market_correlation: f64,
|
|
/// Volatility regime classification
|
|
pub volatility_regime: VolatilityRegime,
|
|
/// Last updated timestamp for volatility metrics
|
|
pub last_updated: DateTime<Utc>,
|
|
/// Number of observations used for calculation
|
|
pub sample_size: u32,
|
|
}
|
|
|
|
impl VolatilityProfile {
|
|
/// Creates a new volatility profile with default values.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
average_volatility: 0.20,
|
|
max_volatility: 1.00,
|
|
min_volatility: 0.05,
|
|
beta: 1.0,
|
|
atr: 0.0,
|
|
market_correlation: 0.0,
|
|
volatility_regime: VolatilityRegime::Normal,
|
|
last_updated: Utc::now(),
|
|
sample_size: 0,
|
|
}
|
|
}
|
|
|
|
/// Updates volatility metrics with new data point.
|
|
pub fn update_metrics(&mut self, new_volatility: f64, new_atr: f64) {
|
|
// Update exponential moving average
|
|
{
|
|
let alpha = 0.1_f64; // Smoothing factor
|
|
#[allow(clippy::float_arithmetic)]
|
|
let one_minus_alpha = 1.0_f64 - alpha;
|
|
#[allow(clippy::float_arithmetic)]
|
|
let volatility_term = one_minus_alpha * self.average_volatility;
|
|
self.average_volatility = alpha.mul_add(new_volatility, volatility_term);
|
|
|
|
#[allow(clippy::float_arithmetic)]
|
|
let atr_term = one_minus_alpha * self.atr;
|
|
self.atr = alpha.mul_add(new_atr, atr_term);
|
|
}
|
|
self.last_updated = Utc::now();
|
|
self.sample_size = self.sample_size.saturating_add(1);
|
|
|
|
// Update volatility regime
|
|
self.volatility_regime = self.classify_regime();
|
|
}
|
|
|
|
/// Classifies current volatility regime based on metrics.
|
|
fn classify_regime(&self) -> VolatilityRegime {
|
|
#[allow(clippy::float_arithmetic)]
|
|
let volatility_ratio = self.average_volatility / 0.20_f64; // Relative to 20% baseline
|
|
|
|
if volatility_ratio > 2.0_f64 {
|
|
VolatilityRegime::High
|
|
} else if volatility_ratio > 1.5 {
|
|
VolatilityRegime::Elevated
|
|
} else if volatility_ratio < 0.5 {
|
|
VolatilityRegime::Low
|
|
} else {
|
|
VolatilityRegime::Normal
|
|
}
|
|
}
|
|
|
|
/// Returns risk-adjusted position size multiplier.
|
|
pub const fn position_size_multiplier(&self) -> f64 {
|
|
match self.volatility_regime {
|
|
VolatilityRegime::Low => 1.5,
|
|
VolatilityRegime::Normal => 1.0,
|
|
VolatilityRegime::Elevated => 0.7,
|
|
VolatilityRegime::High => 0.4,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for VolatilityProfile {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Volatility regime classification for risk management.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum VolatilityRegime {
|
|
/// Low volatility environment (< 50% of normal)
|
|
Low,
|
|
/// Normal volatility environment
|
|
Normal,
|
|
/// Elevated volatility (50-100% above normal)
|
|
Elevated,
|
|
/// High volatility environment (> 100% above normal)
|
|
High,
|
|
}
|
|
|
|
/// Trading hours configuration for different markets and sessions.
|
|
///
|
|
/// Defines market operating hours, pre-market and after-hours sessions,
|
|
/// and holiday schedules for accurate trade timing and execution.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TradingHours {
|
|
/// Primary market timezone identifier (e.g., "America/New_York")
|
|
pub timezone: String,
|
|
/// Regular trading session start time
|
|
pub market_open: NaiveTime,
|
|
/// Regular trading session end time
|
|
pub market_close: NaiveTime,
|
|
/// Pre-market session start time (optional)
|
|
pub pre_market_open: Option<NaiveTime>,
|
|
/// After-hours session end time (optional)
|
|
pub after_hours_close: Option<NaiveTime>,
|
|
/// Trading days of the week
|
|
pub trading_days: Vec<Weekday>,
|
|
/// Market holidays (dates when market is closed)
|
|
pub holidays: Vec<NaiveDate>,
|
|
/// Half-day sessions with early close times
|
|
pub half_days: HashMap<NaiveDate, NaiveTime>,
|
|
}
|
|
|
|
impl TradingHours {
|
|
/// Creates US equity market trading hours configuration.
|
|
#[allow(clippy::unwrap_used)] // Uses hardcoded time values that are guaranteed to be valid
|
|
pub fn us_equity() -> Self {
|
|
Self {
|
|
timezone: "America/New_York".to_owned(),
|
|
market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
|
market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
|
|
pre_market_open: Some(NaiveTime::from_hms_opt(4, 0, 0).unwrap()),
|
|
after_hours_close: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()),
|
|
trading_days: vec![
|
|
Weekday::Mon,
|
|
Weekday::Tue,
|
|
Weekday::Wed,
|
|
Weekday::Thu,
|
|
Weekday::Fri,
|
|
],
|
|
holidays: vec![],
|
|
half_days: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Creates 24/7 trading hours for crypto markets.
|
|
#[allow(clippy::unwrap_used)] // Uses hardcoded time values that are guaranteed to be valid
|
|
pub fn crypto_24_7() -> Self {
|
|
Self {
|
|
timezone: "UTC".to_owned(),
|
|
market_open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
|
market_close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
|
|
pre_market_open: None,
|
|
after_hours_close: None,
|
|
trading_days: vec![
|
|
Weekday::Mon,
|
|
Weekday::Tue,
|
|
Weekday::Wed,
|
|
Weekday::Thu,
|
|
Weekday::Fri,
|
|
Weekday::Sat,
|
|
Weekday::Sun,
|
|
],
|
|
holidays: vec![],
|
|
half_days: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Creates forex market trading hours (Sunday 5 PM to Friday 5 PM EST).
|
|
#[allow(clippy::unwrap_used)] // Uses hardcoded time values that are guaranteed to be valid
|
|
pub fn forex() -> Self {
|
|
Self {
|
|
timezone: "America/New_York".to_owned(),
|
|
market_open: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
|
|
market_close: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
|
|
pre_market_open: None,
|
|
after_hours_close: None,
|
|
trading_days: vec![
|
|
Weekday::Sun,
|
|
Weekday::Mon,
|
|
Weekday::Tue,
|
|
Weekday::Wed,
|
|
Weekday::Thu,
|
|
Weekday::Fri,
|
|
],
|
|
holidays: vec![],
|
|
half_days: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Checks if market is currently open.
|
|
pub fn is_market_open(&self, current_time: DateTime<Utc>) -> bool {
|
|
// Convert to market timezone and check if within trading hours
|
|
// This is a simplified implementation - production would use proper timezone handling
|
|
let current_date = current_time.date_naive();
|
|
let current_time = current_time.time();
|
|
let current_weekday = current_date.weekday();
|
|
|
|
// Check if it's a trading day
|
|
if !self.trading_days.contains(¤t_weekday) {
|
|
return false;
|
|
}
|
|
|
|
// Check if it's a holiday
|
|
if self.holidays.contains(¤t_date) {
|
|
return false;
|
|
}
|
|
|
|
// Check if within trading hours
|
|
current_time >= self.market_open && current_time <= self.market_close
|
|
}
|
|
|
|
/// Checks if extended hours trading is active.
|
|
pub fn is_extended_hours_open(&self, current_time: DateTime<Utc>) -> bool {
|
|
let current_time = current_time.time();
|
|
|
|
// Check pre-market
|
|
if let Some(pre_open) = self.pre_market_open {
|
|
if current_time >= pre_open && current_time < self.market_open {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check after-hours
|
|
if let Some(after_close) = self.after_hours_close {
|
|
if current_time > self.market_close && current_time <= after_close {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
impl Default for TradingHours {
|
|
fn default() -> Self {
|
|
Self::us_equity()
|
|
}
|
|
}
|
|
|
|
/// Comprehensive symbol configuration containing all trading parameters.
|
|
///
|
|
/// Central configuration structure for each tradeable symbol, containing
|
|
/// classification, market parameters, risk settings, and execution rules.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SymbolConfig {
|
|
/// Unique symbol identifier
|
|
pub symbol: String,
|
|
/// Symbol description or company name
|
|
pub description: String,
|
|
/// Asset classification
|
|
pub classification: AssetClassification,
|
|
/// Volatility and risk profile
|
|
pub volatility_profile: VolatilityProfile,
|
|
/// Market operating hours
|
|
pub trading_hours: TradingHours,
|
|
/// Minimum price increment (tick size)
|
|
pub tick_size: f64,
|
|
/// Standard trading unit size
|
|
pub lot_size: f64,
|
|
/// Minimum order quantity
|
|
pub min_order_size: f64,
|
|
/// Maximum order quantity
|
|
pub max_order_size: f64,
|
|
/// Primary exchange or venue
|
|
pub primary_exchange: String,
|
|
/// Currency denomination
|
|
pub currency: String,
|
|
/// Sector classification (for equities)
|
|
pub sector: Option<String>,
|
|
/// Industry classification (for equities)
|
|
pub industry: Option<String>,
|
|
/// Market capitalization (for equities)
|
|
pub market_cap: Option<f64>,
|
|
/// Average daily volume
|
|
pub avg_daily_volume: f64,
|
|
/// Margin requirements
|
|
pub margin_requirement: f64,
|
|
/// Position limits
|
|
pub position_limit: Option<f64>,
|
|
/// Risk multiplier for position sizing
|
|
pub risk_multiplier: f64,
|
|
/// Configuration metadata
|
|
pub metadata: SymbolMetadata,
|
|
}
|
|
|
|
impl SymbolConfig {
|
|
/// Creates a new symbol configuration with default values.
|
|
pub fn new(symbol: String, classification: AssetClassification) -> Self {
|
|
let trading_hours = match classification {
|
|
AssetClassification::Crypto => TradingHours::crypto_24_7(),
|
|
AssetClassification::Forex => TradingHours::forex(),
|
|
_ => TradingHours::us_equity(),
|
|
};
|
|
|
|
Self {
|
|
symbol: symbol.clone(),
|
|
description: format!("{} - Auto-generated", symbol),
|
|
classification,
|
|
volatility_profile: VolatilityProfile::new(),
|
|
trading_hours,
|
|
tick_size: 0.01,
|
|
lot_size: 1.0,
|
|
min_order_size: 1.0,
|
|
max_order_size: 1_000_000.0,
|
|
primary_exchange: "".to_owned(),
|
|
currency: "USD".to_owned(),
|
|
sector: None,
|
|
industry: None,
|
|
market_cap: None,
|
|
avg_daily_volume: 0.0,
|
|
margin_requirement: 0.25,
|
|
position_limit: None,
|
|
risk_multiplier: 1.0,
|
|
metadata: SymbolMetadata::new(),
|
|
}
|
|
}
|
|
|
|
/// Validates the symbol configuration for correctness.
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.symbol.is_empty() {
|
|
return Err("Symbol cannot be empty".to_owned());
|
|
}
|
|
|
|
if self.tick_size <= 0.0_f64 {
|
|
return Err("Tick size must be positive".to_owned());
|
|
}
|
|
|
|
if self.lot_size <= 0.0_f64 {
|
|
return Err("Lot size must be positive".to_owned());
|
|
}
|
|
|
|
if self.min_order_size <= 0.0_f64 {
|
|
return Err("Minimum order size must be positive".to_owned());
|
|
}
|
|
|
|
if self.max_order_size <= self.min_order_size {
|
|
return Err("Maximum order size must be greater than minimum".to_owned());
|
|
}
|
|
|
|
if self.margin_requirement < 0.0_f64 || self.margin_requirement > 1.0_f64 {
|
|
return Err("Margin requirement must be between 0 and 1".to_owned());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculates the effective position size based on risk parameters.
|
|
pub fn calculate_position_size(&self, base_size: f64, _account_value: f64) -> f64 {
|
|
let volatility_multiplier = self.volatility_profile.position_size_multiplier();
|
|
let risk_adjusted_size = base_size
|
|
.mul_add(volatility_multiplier, 0.0)
|
|
.mul_add(self.risk_multiplier, 0.0);
|
|
|
|
// Apply position limits
|
|
if let Some(limit) = self.position_limit {
|
|
risk_adjusted_size.min(limit)
|
|
} else {
|
|
risk_adjusted_size
|
|
}
|
|
}
|
|
|
|
/// Returns the appropriate tick size for a given price level.
|
|
pub const fn get_tick_size_for_price(&self, _price: f64) -> f64 {
|
|
// Some markets have variable tick sizes based on price
|
|
// This is a simplified implementation
|
|
self.tick_size
|
|
}
|
|
|
|
/// Rounds price to the nearest valid tick.
|
|
pub fn round_to_tick(&self, price: f64) -> f64 {
|
|
let tick = self.get_tick_size_for_price(price);
|
|
#[allow(clippy::float_arithmetic)]
|
|
let result = (price / tick).round() * tick;
|
|
result
|
|
}
|
|
|
|
/// Checks if the symbol is currently tradeable.
|
|
pub fn is_tradeable(&self, current_time: DateTime<Utc>) -> bool {
|
|
self.trading_hours.is_market_open(current_time) && self.metadata.is_active
|
|
}
|
|
|
|
/// Checks if extended hours trading is available.
|
|
pub const fn supports_extended_hours(&self) -> bool {
|
|
self.classification.supports_extended_hours()
|
|
}
|
|
}
|
|
|
|
/// Symbol configuration metadata for versioning and tracking.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SymbolMetadata {
|
|
/// Unique configuration ID
|
|
pub id: Uuid,
|
|
/// Configuration version
|
|
pub version: u32,
|
|
/// Creation timestamp
|
|
pub created_at: DateTime<Utc>,
|
|
/// Last update timestamp
|
|
pub updated_at: DateTime<Utc>,
|
|
/// Active status
|
|
pub is_active: bool,
|
|
/// Data source for configuration
|
|
pub data_source: String,
|
|
/// Last validation timestamp
|
|
pub last_validated: Option<DateTime<Utc>>,
|
|
/// Configuration tags for organization
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
impl SymbolMetadata {
|
|
/// Creates new metadata with default values.
|
|
pub fn new() -> Self {
|
|
let now = Utc::now();
|
|
Self {
|
|
id: Uuid::new_v4(),
|
|
version: 1,
|
|
created_at: now,
|
|
updated_at: now,
|
|
is_active: true,
|
|
data_source: "manual".to_owned(),
|
|
last_validated: None,
|
|
tags: vec![],
|
|
}
|
|
}
|
|
|
|
/// Updates the metadata timestamp and version.
|
|
pub fn update(&mut self) {
|
|
self.updated_at = Utc::now();
|
|
self.version = self.version.saturating_add(1);
|
|
}
|
|
|
|
/// Marks the configuration as validated.
|
|
pub fn mark_validated(&mut self) {
|
|
self.last_validated = Some(Utc::now());
|
|
}
|
|
}
|
|
|
|
impl Default for SymbolMetadata {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Symbol configuration manager for loading and caching symbol configurations.
|
|
///
|
|
/// Provides high-performance access to symbol configurations with caching,
|
|
/// hot-reload capabilities, and configuration validation.
|
|
#[derive(Debug)]
|
|
#[allow(clippy::module_name_repetitions)]
|
|
pub struct SymbolConfigManager {
|
|
/// In-memory cache of symbol configurations
|
|
symbol_cache: HashMap<String, SymbolConfig>,
|
|
/// Last cache update timestamp
|
|
last_updated: DateTime<Utc>,
|
|
/// Cache timeout duration
|
|
cache_timeout: Duration,
|
|
}
|
|
|
|
impl SymbolConfigManager {
|
|
/// Creates a new symbol configuration manager.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
symbol_cache: HashMap::new(),
|
|
last_updated: Utc::now(),
|
|
cache_timeout: Duration::from_secs(300), // 5 minutes
|
|
}
|
|
}
|
|
|
|
/// Loads symbol configuration from cache or source.
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn get_symbol_config(
|
|
&mut self,
|
|
symbol: &str,
|
|
) -> Result<Option<SymbolConfig>, String> {
|
|
// Check cache first
|
|
if let Some(config) = self.symbol_cache.get(symbol) {
|
|
if !self.is_cache_expired() {
|
|
return Ok(Some(config.clone()));
|
|
}
|
|
}
|
|
|
|
// Load from source (this would integrate with database/external source)
|
|
self.load_symbol_from_source(symbol).await
|
|
}
|
|
|
|
/// Loads all symbol configurations into cache.
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub async fn load_all_symbols(&mut self) -> Result<usize, String> {
|
|
// This would integrate with the database or external configuration source
|
|
self.refresh_cache().await
|
|
}
|
|
|
|
/// Adds or updates a symbol configuration.
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
pub fn upsert_symbol_config(&mut self, config: SymbolConfig) -> Result<(), String> {
|
|
// Validate configuration
|
|
config.validate()?;
|
|
|
|
// Update cache
|
|
self.symbol_cache.insert(config.symbol.clone(), config);
|
|
self.last_updated = Utc::now();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes a symbol configuration.
|
|
pub fn remove_symbol_config(&mut self, symbol: &str) -> Option<SymbolConfig> {
|
|
self.symbol_cache.remove(symbol)
|
|
}
|
|
|
|
/// Returns all cached symbol configurations.
|
|
pub fn get_all_symbols(&self) -> Vec<&SymbolConfig> {
|
|
self.symbol_cache.values().collect()
|
|
}
|
|
|
|
/// Returns symbols filtered by asset classification.
|
|
pub fn get_symbols_by_classification(
|
|
&self,
|
|
classification: &AssetClassification,
|
|
) -> Vec<&SymbolConfig> {
|
|
self.symbol_cache
|
|
.values()
|
|
.filter(|config| &config.classification == classification)
|
|
.collect()
|
|
}
|
|
|
|
/// Checks if cache has expired.
|
|
fn is_cache_expired(&self) -> bool {
|
|
Utc::now()
|
|
.signed_duration_since(self.last_updated)
|
|
.to_std()
|
|
.unwrap_or(Duration::MAX)
|
|
> self.cache_timeout
|
|
}
|
|
|
|
/// Loads symbol configuration from external source.
|
|
async fn load_symbol_from_source(
|
|
&mut self,
|
|
_symbol: &str,
|
|
) -> Result<Option<SymbolConfig>, String> {
|
|
// This would integrate with database or external configuration API
|
|
// For now, return None to indicate symbol not found
|
|
|
|
// Example of creating a default config if needed:
|
|
// let config = SymbolConfig::new(symbol.to_owned(), AssetClassification::Equity);
|
|
// self.symbol_cache.insert(symbol.to_owned(), config.clone());
|
|
// Ok(Some(config))
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Refreshes the entire symbol cache from source.
|
|
async fn refresh_cache(&mut self) -> Result<usize, String> {
|
|
// This would integrate with database to load all active symbols
|
|
// For now, return the current cache size
|
|
Ok(self.symbol_cache.len())
|
|
}
|
|
|
|
/// Sets cache timeout duration.
|
|
pub const fn set_cache_timeout(&mut self, timeout: Duration) {
|
|
self.cache_timeout = timeout;
|
|
}
|
|
|
|
/// Forces cache refresh on next access.
|
|
pub const fn invalidate_cache(&mut self) {
|
|
self.last_updated = DateTime::<Utc>::MIN_UTC;
|
|
}
|
|
|
|
/// Returns cache statistics.
|
|
pub fn cache_stats(&self) -> (usize, DateTime<Utc>, bool) {
|
|
(
|
|
self.symbol_cache.len(),
|
|
self.last_updated,
|
|
self.is_cache_expired(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Default for SymbolConfigManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_asset_classification_regulatory_class() {
|
|
assert_eq!(AssetClassification::Equity.regulatory_class(), "EQUITY");
|
|
assert_eq!(AssetClassification::Forex.regulatory_class(), "FX");
|
|
assert_eq!(AssetClassification::Crypto.regulatory_class(), "CRYPTO");
|
|
}
|
|
|
|
#[test]
|
|
fn test_volatility_profile_update() {
|
|
let mut profile = VolatilityProfile::new();
|
|
profile.update_metrics(0.40, 2.5);
|
|
|
|
// With exponential smoothing: 0.1 * 0.40 + 0.9 * 0.20 = 0.22
|
|
assert!(profile.average_volatility > 0.20 && profile.average_volatility < 0.25);
|
|
// With exponential smoothing: 0.1 * 2.5 + 0.9 * 0.0 = 0.25
|
|
assert!((profile.atr - 0.25).abs() < 0.01);
|
|
assert_eq!(profile.volatility_regime, VolatilityRegime::Normal);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_config_validation() {
|
|
let mut config = SymbolConfig::new("AAPL".to_owned(), AssetClassification::Equity);
|
|
assert!(config.validate().is_ok());
|
|
|
|
config.tick_size = -0.01;
|
|
assert!(config.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_trading_hours_us_equity() {
|
|
let hours = TradingHours::us_equity();
|
|
assert_eq!(hours.timezone, "America/New_York");
|
|
assert_eq!(
|
|
hours.market_open,
|
|
NaiveTime::from_hms_opt(9, 30, 0).unwrap()
|
|
);
|
|
assert_eq!(
|
|
hours.market_close,
|
|
NaiveTime::from_hms_opt(16, 0, 0).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbol_config_manager() {
|
|
let mut manager = SymbolConfigManager::new();
|
|
let config = SymbolConfig::new("TEST".to_owned(), AssetClassification::Equity);
|
|
|
|
assert!(manager.upsert_symbol_config(config).is_ok());
|
|
assert_eq!(manager.get_all_symbols().len(), 1);
|
|
}
|
|
}
|