Files
foxhunt/config/src/symbol_config.rs
jgrusewski c2687bf084 chore(clippy): add deny(unwrap_used) to config and trading_agent_service, fix 27 violations
- 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>
2026-02-22 00:01:40 +01:00

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