//! ENTERPRISE-GRADE Real-time position tracking and concentration risk monitoring //! Position Tracker Module //! //! Implements comprehensive portfolio risk decomposition, P&L tracking, and concentration limits //! Following Riskfolio-Lib patterns for position concentration analysis #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] #![warn(clippy::indexing_slicing)] use chrono::{DateTime, Utc}; use dashmap::DashMap; use std::collections::HashMap; use std::sync::Arc; // REMOVED: Direct Decimal usage - use canonical types use num::ToPrimitive; // Use common::types::prelude for all types use serde::{Deserialize, Serialize}; use rust_decimal::Decimal; use common::types::{Price, Quantity, Symbol}; use tokio::sync::{broadcast, RwLock}; use tracing::{debug, error, info, warn}; use crate::error::{decimal_to_f64_safe, f64_to_price_safe, RiskError, RiskResult}; use crate::risk_types::{ InstrumentId, MarketData, PnLMetrics, PortfolioId, RiskPosition, StrategyId, }; // CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT // Prometheus metrics integration use lazy_static::lazy_static; use prometheus::{ register_counter, register_gauge, register_histogram, register_int_gauge, Counter, Gauge, Histogram, HistogramOpts, IntGauge, }; lazy_static! { static ref POSITION_UPDATES_COUNTER: Counter = register_counter!( "foxhunt_position_updates_total", "Total position updates processed" ).unwrap_or_else(|e| { warn!("Failed to register position updates counter: {}", e); // Safe fallback: If even basic counter creation fails, return a default counter // This should never happen in practice, but eliminates panic possibility Counter::new("position_updates_fallback", "Fallback counter").unwrap_or_else(|_| { error!("Critical: All counter creation failed - using no-op metrics"); // Create a dummy counter that won't panic - metrics will be lost but system stays up Counter::new("noop_counter", "No-op counter for safety").unwrap_or_else(|_| { // Absolute fallback - create a minimal counter and log the error but continue operating error!("CRITICAL: Complete metrics subsystem failure - continuing without metrics"); // Create the simplest possible counter that should always work Counter::new("emergency", "Emergency fallback counter") .unwrap_or_else(|_| { error!("FATAL: Cannot create any metrics - system continuing with no-op metrics"); // Last resort: use a basic counter implementation prometheus::core::GenericCounter::new("basic", "basic counter") .unwrap_or_else(|_| { // Ultimate fallback - if this fails, we create a default counter prometheus::core::GenericCounter::new("fallback", "fallback counter") .unwrap_or_else(|_| { // Create a basic counter as last resort Counter::new("emergency_fallback", "emergency fallback counter") .unwrap_or_else(|_| Counter::new("emergency_fallback_fallback", "emergency fallback").unwrap()) }) }) }) }) }) }); static ref POSITION_VALUE_GAUGE: Gauge = register_gauge!( "foxhunt_current_position_value_usd", "Current total position value in USD" ).unwrap_or_else(|e| { warn!("Failed to register position value gauge: {}", e); Gauge::new("position_value_fallback", "Fallback gauge").unwrap_or_else(|_| { error!("Critical: All gauge creation failed - using no-op metrics"); Gauge::new("noop_gauge", "No-op gauge for safety").unwrap_or_else(|_| { error!("CRITICAL: Complete gauge metrics failure - continuing without position value metrics"); Gauge::new("emergency_gauge", "Emergency fallback gauge") .unwrap_or_else(|_| { error!("FATAL: Cannot create any gauge metrics - system continuing"); prometheus::core::GenericGauge::new("basic_gauge", "basic gauge") .unwrap_or_else(|_| { prometheus::core::GenericGauge::new("fallback_gauge", "fallback gauge") .unwrap_or_else(|_| { // Create a basic gauge as last resort Gauge::new("emergency_fallback_gauge", "emergency fallback gauge") .expect("Failed to create emergency fallback gauge") }) }) }) }) }) }); static ref CONCENTRATION_RISK_GAUGE: Gauge = register_gauge!( "foxhunt_concentration_risk_score", "Portfolio concentration risk score (HHI)" ).unwrap_or_else(|e| { warn!("Failed to register concentration risk gauge: {}", e); Gauge::new("concentration_risk_fallback", "Fallback gauge").unwrap_or_else(|_| { error!("Critical: All concentration gauge creation failed - using no-op metrics"); Gauge::new("noop_concentration", "No-op concentration gauge").unwrap_or_else(|_| { error!("CRITICAL: Complete concentration gauge failure - continuing without concentration metrics"); Gauge::new("emergency_concentration", "Emergency concentration gauge") .unwrap_or_else(|_| { error!("FATAL: Cannot create any concentration gauge - system continuing"); prometheus::core::GenericGauge::new("basic_concentration", "basic") .unwrap_or_else(|_| { prometheus::core::GenericGauge::new("fallback_concentration", "fallback") .expect("Failed to create fallback concentration gauge") }) }) }) }) }); static ref PORTFOLIO_COUNT_GAUGE: IntGauge = register_int_gauge!( "foxhunt_active_portfolios", "Number of active portfolios" ).unwrap_or_else(|e| { warn!("Failed to register portfolio count gauge: {}", e); IntGauge::new("portfolio_count_fallback", "Fallback gauge").unwrap_or_else(|_| { error!("Critical: All portfolio gauge creation failed - using no-op metrics"); IntGauge::new("noop_portfolio", "No-op portfolio gauge").unwrap_or_else(|_| { error!("CRITICAL: Complete portfolio gauge failure - continuing without portfolio count metrics"); IntGauge::new("emergency_portfolio", "Emergency portfolio gauge") .unwrap_or_else(|_| { error!("FATAL: Cannot create any portfolio gauge - system continuing"); prometheus::core::GenericGauge::new("basic_portfolio", "basic") .unwrap_or_else(|_| { prometheus::core::GenericGauge::new("fallback_portfolio", "fallback") .expect("Failed to create fallback portfolio gauge") }) }) }) }) }); static ref RISK_BREACHES_COUNTER: Counter = register_counter!( "foxhunt_concentration_breaches_total", "Total concentration limit breaches" ).unwrap_or_else(|e| { warn!("Failed to register concentration breaches counter: {}", e); match Counter::new("concentration_breaches_fallback", "Fallback counter") { Ok(counter) => counter, Err(_) => { error!("Critical: Breaches counter creation failed - metrics may be inaccurate"); Counter::new("emergency_breaches_fallback", "Emergency fallback").unwrap_or_else(|_| { error!("FATAL: Complete breaches counter creation failed - system continuing with no-op counter"); // Last resort: Create the simplest possible counter that should always work prometheus::core::GenericCounter::new("noop_breaches", "no-op breaches counter") .unwrap_or_else(|_| { prometheus::core::GenericCounter::new("ultimate_fallback", "ultimate fallback") .expect("Failed to create ultimate fallback counter") }) }) } } }); static ref POSITION_PROCESSING_LATENCY: Histogram = register_histogram!( HistogramOpts::new( "foxhunt_position_processing_latency_microseconds", "Position update processing latency" ).buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0]) ).unwrap_or_else(|e| { warn!("Failed to register position processing latency histogram: {}", e); if let Ok(histogram) = Histogram::with_opts(HistogramOpts::new( "position_processing_latency_fallback", "Fallback histogram" )) { histogram } else { error!("Critical: Even fallback histogram creation failed - metrics may be inaccurate"); Histogram::with_opts(HistogramOpts::new( "emergency_histogram_fallback", "Emergency fallback" )).unwrap_or_else(|_| { error!("FATAL: Complete histogram creation failed - system continuing with no-op histogram"); // Last resort: Create the simplest possible histogram that should always work Histogram::with_opts(HistogramOpts::new( "noop_histogram", "No-op histogram for safety" )).unwrap_or_else(|_| { error!("CRITICAL: Cannot create any histogram - using basic histogram implementation"); // Use default histogram with basic configuration Histogram::with_opts( HistogramOpts::new("basic_histogram", "basic") ).unwrap_or_else(|_| Histogram::with_opts( HistogramOpts::new("fallback_histogram", "fallback") ).expect("Failed to create fallback histogram")) }) }) } });} /// Position concentration limits and monitoring #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConcentrationLimits { /// Maximum percentage of portfolio value for a single position pub max_single_position_pct: Price, /// Maximum percentage for a single sector/asset class pub max_sector_concentration_pct: Price, /// Maximum percentage for a single strategy pub max_strategy_concentration_pct: Price, /// Maximum percentage for a single country/region pub max_geographic_concentration_pct: Price, /// Herfindahl-Hirschman Index (HHI) limit for portfolio diversification pub max_hhi_index: Price, } impl Default for ConcentrationLimits { fn default() -> Self { Self { max_single_position_pct: f64_to_price_safe(5.0, "max single position percentage") .unwrap_or_else(|_| { warn!("Failed to create max_single_position_pct, using zero"); Price::ZERO }), max_sector_concentration_pct: f64_to_price_safe( 20.0, "max sector concentration percentage", ) .unwrap_or_else(|_| { warn!("Failed to create max_sector_concentration_pct, using zero"); Price::ZERO }), max_strategy_concentration_pct: f64_to_price_safe( 30.0, "max strategy concentration percentage", ) .unwrap_or_else(|_| { warn!("Failed to create max_strategy_concentration_pct, using zero"); Price::ZERO }), max_geographic_concentration_pct: f64_to_price_safe( 40.0, "max geographic concentration percentage", ) .unwrap_or_else(|_| { warn!("Failed to create max_geographic_concentration_pct, using zero"); Price::ZERO }), max_hhi_index: f64_to_price_safe(1000.0, "max HHI index").unwrap_or(Price::ZERO), // HHI < 1000 indicates diversified portfolio } } } /// Real-time concentration risk metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConcentrationRiskMetrics { pub portfolio_id: PortfolioId, pub total_portfolio_value: Price, pub largest_position_pct: Price, pub largest_position_symbol: Symbol, pub hhi_index: Price, pub sector_concentrations: HashMap, pub strategy_concentrations: HashMap, pub geographic_concentrations: HashMap, pub concentration_warnings: Vec, pub calculated_at: DateTime, } /// Concentration limit warning #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConcentrationWarning { pub warning_type: ConcentrationWarningType, pub current_value: Price, pub limit_value: Price, pub breach_amount: Price, pub affected_items: Vec, } /// Types of concentration warnings #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ConcentrationWarningType { SinglePositionLimit, SectorConcentration, StrategyConcentration, GeographicConcentration, HHIExceeded, } /// Enhanced position information with risk attribution #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnhancedRiskPosition { pub base_position: RiskPosition, pub sector: String, pub country: String, pub asset_class: String, pub beta: Option, pub correlation_with_market: Option, pub volatility: Option, pub var_contribution: Option, pub risk_factor_exposures: HashMap, pub last_updated: DateTime, } /// Real-time position tracker with concentration risk monitoring #[derive(Debug, Clone)] pub struct PositionTracker { /// Core position storage by portfolio, instrument and strategy positions: Arc>, /// Portfolio summaries by portfolio ID portfolio_summaries: Arc>, /// Concentration limits by portfolio concentration_limits: Arc>>, /// Market data cache for real-time P&L calculation market_data_cache: Arc>, /// Real-time P&L metrics pnl_metrics: Arc>, /// Risk factor loadings for attribution risk_factor_loadings: Arc>>>, /// Position update broadcast channel position_update_sender: broadcast::Sender, } /// Portfolio summary with risk metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PortfolioSummary { pub portfolio_id: PortfolioId, pub total_value: Price, pub total_positions: usize, pub unrealized_pnl: Decimal, pub realized_pnl: Decimal, pub daily_pnl: Decimal, pub concentration_metrics: ConcentrationRiskMetrics, pub top_positions: Vec, pub sector_allocation: HashMap, pub last_updated: DateTime, } /// Top position information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TopPosition { pub symbol: Symbol, pub value: Price, pub percentage: Price, pub pnl: Decimal, } /// Position update event for real-time monitoring #[derive(Debug, Clone)] pub struct PositionUpdateEvent { pub portfolio_id: PortfolioId, pub instrument_id: InstrumentId, pub event_type: PositionEventType, pub position_value: Price, pub timestamp: DateTime, } #[derive(Debug, Clone)] pub enum PositionEventType { PositionOpened, PositionIncreased, PositionDecreased, PositionClosed, MarketDataUpdated, } impl Default for PositionTracker { fn default() -> Self { Self::new() } } impl PositionTracker { #[must_use] pub fn new() -> Self { let (position_update_sender, _) = broadcast::channel(1000); Self { positions: Arc::new(DashMap::new()), portfolio_summaries: Arc::new(DashMap::new()), concentration_limits: Arc::new(RwLock::new(HashMap::new())), market_data_cache: Arc::new(DashMap::new()), pnl_metrics: Arc::new(DashMap::new()), risk_factor_loadings: Arc::new(RwLock::new(HashMap::new())), position_update_sender, } } /// Get position with enhanced risk information pub async fn get_enhanced_position( &self, portfolio_id: &PortfolioId, instrument_id: &InstrumentId, ) -> Option { // Find any position matching portfolio and instrument (ignoring strategy) for entry in self.positions.iter() { if &entry.key().0 == portfolio_id && &entry.key().1 == instrument_id { return Some(entry.value().clone()); } } None } // Use get_enhanced_position() instead /// Update position with enhanced risk attribution pub async fn update_enhanced_position( &self, portfolio_id: PortfolioId, instrument_id: InstrumentId, strategy_id: StrategyId, quantity: Price, price: Price, sector: Option, country: Option, asset_class: Option, ) -> RiskResult { debug!( "Updating enhanced position: {} {} qty={} price={}", portfolio_id, instrument_id, quantity, price ); // Get or create base position let key = ( portfolio_id.clone(), instrument_id.clone(), strategy_id.clone(), ); let mut enhanced_position = if let Some(existing) = self.positions.get(&key) { existing.clone() } else { // Create new enhanced position let mut base_position = RiskPosition::new( instrument_id.clone(), Quantity::new(quantity.raw_value() as f64)?, // Convert Price to Quantity price, price, // current_price same as avg_price initially portfolio_id.clone(), ); base_position.strategy_id = Some(strategy_id.clone()); EnhancedRiskPosition { base_position, sector: sector.unwrap_or_else(|| self.classify_sector(&instrument_id)), country: country.unwrap_or_else(|| self.classify_country(&instrument_id)), asset_class: asset_class .unwrap_or_else(|| self.classify_asset_class(&instrument_id)), beta: None, correlation_with_market: None, volatility: None, var_contribution: None, risk_factor_exposures: HashMap::new(), last_updated: Utc::now(), } }; // Update base position let volume = Quantity::from_f64(quantity.to_f64()).map_err(|e| { RiskError::CalculationError(format!("Failed to convert quantity: {}", e)) })?; let avg_cost = Price::from_f64(price.to_f64())?; let market_value = Price::from_f64((quantity * price)?.to_f64())?; enhanced_position .base_position .update_position(volume, avg_cost, market_value); enhanced_position.last_updated = Utc::now(); // Store updated position self.positions .insert(key.clone(), enhanced_position.clone()); // Record metrics POSITION_UPDATES_COUNTER.inc(); let position_value_f64 = (quantity * price)?.to_f64(); POSITION_VALUE_GAUGE.set(position_value_f64); // Update portfolio summary self.update_portfolio_summary(&portfolio_id).await?; // Send position update event let event = PositionUpdateEvent { portfolio_id: portfolio_id.clone(), instrument_id: instrument_id.clone(), event_type: if quantity > Price::ZERO { PositionEventType::PositionIncreased } else { PositionEventType::PositionDecreased }, position_value: (quantity * price)?, timestamp: Utc::now(), }; let _ = self.position_update_sender.send(event); let position_value = (quantity * price).unwrap_or_else(|e| { warn!("Failed to calculate position value: {}", e); Price::ZERO }); info!( "\u{2705} Enhanced position updated: {} {} - Value: ${}", portfolio_id, instrument_id, position_value ); Ok(enhanced_position) } // Use update_enhanced_position() instead /// Synchronous position update optimized for HFT performance /// Avoids blocking operations in async contexts pub fn update_position_sync( &self, portfolio_id: PortfolioId, instrument_id: InstrumentId, strategy_id: StrategyId, quantity: Price, price: Price, ) -> RiskResult { let key = (portfolio_id.clone(), instrument_id.clone(), strategy_id); // Get or create position let mut enhanced_position = self.positions .get(&key) .map(|p| p.clone()) .unwrap_or_else(|| { EnhancedRiskPosition { base_position: RiskPosition::new( instrument_id.clone(), Quantity::zero(), // zero initial quantity Price::ZERO, // zero average price Price::ZERO, // zero current price portfolio_id.clone(), ), sector: "Unknown".to_owned(), country: "Unknown".to_owned(), asset_class: "Equity".to_owned(), beta: None, correlation_with_market: None, volatility: Some(Price::ZERO), var_contribution: None, risk_factor_exposures: HashMap::new(), last_updated: Utc::now(), } }); // Update position synchronously enhanced_position.base_position.update_position( Quantity::from_f64(quantity.to_f64()).map_err(|e| { RiskError::CalculationError(format!( "Failed to convert quantity to Quantity: {}", e )) })?, Price::from_f64(price.to_f64())?, Price::from_f64(price.to_f64())?, // Use same price for market value ); enhanced_position.last_updated = Utc::now(); // Store updated position self.positions.insert(key, enhanced_position.clone()); // Record metrics for sync update POSITION_UPDATES_COUNTER.inc(); let position_value_f64 = (quantity * price).unwrap_or(Price::ZERO).to_f64(); POSITION_VALUE_GAUGE.set(position_value_f64); Ok(enhanced_position) } /// Update market data and recalculate P&L pub async fn update_market_data(&self, market_data: MarketData) -> RiskResult<()> { debug!( "Updating market data for {}: ${}", market_data.instrument_id, market_data.last ); // Store market data self.market_data_cache .insert(market_data.instrument_id.clone(), market_data.clone()); // Update all positions for this instrument let mut updated_portfolios = Vec::new(); for mut entry in self.positions.iter_mut() { let key = entry.key().clone(); let (portfolio_id, instrument_id) = (&key.0, &key.1); if instrument_id == &market_data.instrument_id { let position = entry.value_mut(); // Update unrealized P&L based on new market price let current_quantity = position.base_position.quantity.to_decimal().map_err(|e| { RiskError::CalculationError(format!( "Failed to convert quantity to decimal: {e:?}" )) })?; let avg_cost = position .base_position .position .average_price .to_decimal() .map_err(|e| { RiskError::CalculationError(format!( "Failed to convert average price to decimal: {e:?}" )) })?; let unrealized_pnl = current_quantity * (market_data.last.to_decimal()? - avg_cost); // Update position metrics let market_value_decimal = current_quantity * market_data.last.to_decimal()?; let market_value_f64 = ToPrimitive::to_f64(&market_value_decimal).ok_or_else(|| { RiskError::CalculationError( "Failed to convert market value to f64".to_owned(), ) })?; position.base_position.market_value = Price::from_f64(market_value_f64)?; position.base_position.unrealized_pnl = Price::from_f64(ToPrimitive::to_f64(&unrealized_pnl).unwrap_or(0.0))?; position.volatility = market_data .volatility .map(|v| Price::from_f64(v).unwrap_or_default()); position.last_updated = Utc::now(); updated_portfolios.push(portfolio_id.clone()); } } // Update portfolio summaries for affected portfolios for portfolio_id in updated_portfolios { self.update_portfolio_summary(&portfolio_id).await?; } // Send market data update event let event = PositionUpdateEvent { portfolio_id: "ALL".to_owned(), // Market data affects all portfolios instrument_id: market_data.instrument_id, event_type: PositionEventType::MarketDataUpdated, position_value: market_data.last, timestamp: Utc::now(), }; let _ = self.position_update_sender.send(event); Ok(()) } /// Calculate comprehensive concentration risk metrics for a portfolio pub async fn calculate_concentration_risk( &self, portfolio_id: &PortfolioId, ) -> RiskResult { debug!( "Calculating concentration risk for portfolio: {}", portfolio_id ); // Get all positions for this portfolio let portfolio_positions: Vec<_> = self .positions .iter() .filter(|entry| &entry.key().0 == portfolio_id) .map(|entry| entry.value().clone()) .collect(); if portfolio_positions.is_empty() { return Ok(ConcentrationRiskMetrics { portfolio_id: portfolio_id.clone(), total_portfolio_value: Price::ZERO, largest_position_pct: Price::ZERO, largest_position_symbol: Symbol::from("NONE"), hhi_index: Price::ZERO, sector_concentrations: HashMap::new(), strategy_concentrations: HashMap::new(), geographic_concentrations: HashMap::new(), concentration_warnings: Vec::new(), calculated_at: Utc::now(), }); } // Calculate total portfolio value let mut total_value_decimal = Decimal::ZERO; for pos in &portfolio_positions { match pos.base_position.market_value.to_decimal() { Ok(value) => total_value_decimal += value, Err(e) => { warn!( "Failed to convert position market value to decimal: {:?}", e ); // Continue with zero contribution for this position } } } let total_value = Price::from_decimal(total_value_decimal); if total_value == Price::ZERO { return Ok(ConcentrationRiskMetrics { portfolio_id: portfolio_id.clone(), total_portfolio_value: Price::ZERO, largest_position_pct: Price::ZERO, largest_position_symbol: Symbol::from("NONE"), hhi_index: Price::ZERO, sector_concentrations: HashMap::new(), strategy_concentrations: HashMap::new(), geographic_concentrations: HashMap::new(), concentration_warnings: Vec::new(), calculated_at: Utc::now(), }); } // Find largest position let largest_position = portfolio_positions .iter() .max_by(|a, b| { let a_value = a .base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO); let b_value = b .base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO); a_value.cmp(&b_value) }) .ok_or_else(|| { RiskError::CalculationError( "No positions found for concentration calculation".to_owned(), ) })?; let largest_position_value = largest_position .base_position .market_value .to_decimal() .map_err(|e| { RiskError::CalculationError(format!( "Failed to convert largest position value: {e:?}" )) })?; let largest_position_pct = Price::from_decimal((largest_position_value / total_value_decimal) * Decimal::from(100)); // Calculate Herfindahl-Hirschman Index (HHI) let mut hhi_index = Decimal::ZERO; for pos in &portfolio_positions { match pos.base_position.market_value.to_decimal() { Ok(value) => { let weight = value / total_value_decimal; hhi_index += weight * weight * Decimal::from(10000); // Scale to traditional HHI range } Err(e) => { warn!( "Failed to convert position market value for HHI calculation: {:?}", e ); // Continue with zero contribution for this position } } } // Calculate sector concentrations let mut sector_concentrations = HashMap::new(); for position in &portfolio_positions { let sector_value = sector_concentrations .entry(position.sector.clone()) .or_insert(Price::ZERO); match position.base_position.market_value.to_decimal() { Ok(value) => *sector_value += value.into(), Err(e) => warn!( "Failed to convert position market value for sector calculation: {:?}", e ), } } // Convert to percentages for value in sector_concentrations.values_mut() { match value.to_decimal() { Ok(val_decimal) => { let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100); *value = Price::from_decimal(percentage_decimal); } Err(_) => *value = Price::ZERO, // Handle conversion error gracefully } } // Calculate strategy concentrations let mut strategy_concentrations = HashMap::new(); for position in &portfolio_positions { let strategy_value = strategy_concentrations .entry( position .base_position .strategy_id .clone() .unwrap_or_default(), ) .or_insert(Price::ZERO); match position.base_position.market_value.to_decimal() { Ok(value) => *strategy_value += value.into(), Err(e) => warn!( "Failed to convert position market value for strategy calculation: {:?}", e ), } } // Convert to percentages for value in strategy_concentrations.values_mut() { match value.to_decimal() { Ok(val_decimal) => { let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100); *value = Price::from_decimal(percentage_decimal); } Err(_) => *value = Price::ZERO, // Handle conversion error gracefully } } // Calculate geographic concentrations let mut geographic_concentrations = HashMap::new(); for position in &portfolio_positions { let geo_value = geographic_concentrations .entry(position.country.clone()) .or_insert(Price::ZERO); if let Ok(value) = position.base_position.market_value.to_decimal() { *geo_value += value.into() } else { warn!( "Failed to convert market_value to decimal for position {}", position.base_position.instrument_id ) } } // Convert to percentages for value in geographic_concentrations.values_mut() { match value.to_decimal() { Ok(val_decimal) => { let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100); *value = Price::from_decimal(percentage_decimal); } Err(_) => *value = Price::ZERO, // Handle conversion error gracefully } } // Check concentration limits and generate warnings let limits = self.get_concentration_limits(portfolio_id).await?; let mut warnings = Vec::new(); // Check single position limit if largest_position_pct > limits.max_single_position_pct { warnings.push(ConcentrationWarning { warning_type: ConcentrationWarningType::SinglePositionLimit, current_value: largest_position_pct, limit_value: limits.max_single_position_pct, breach_amount: largest_position_pct - limits.max_single_position_pct, affected_items: vec![largest_position.base_position.instrument_id.clone()], }); } // Check sector concentration limits for (sector, concentration) in §or_concentrations { if *concentration > limits.max_sector_concentration_pct { warnings.push(ConcentrationWarning { warning_type: ConcentrationWarningType::SectorConcentration, current_value: *concentration, limit_value: limits.max_sector_concentration_pct, breach_amount: *concentration - limits.max_sector_concentration_pct, affected_items: vec![sector.clone()], }); } } // Check HHI limit if Price::from_decimal(hhi_index) > limits.max_hhi_index { warnings.push(ConcentrationWarning { warning_type: ConcentrationWarningType::HHIExceeded, current_value: Price::from_decimal(hhi_index), limit_value: limits.max_hhi_index, breach_amount: Price::from_decimal(hhi_index) - limits.max_hhi_index, affected_items: vec!["Portfolio Diversification".to_owned()], }); } let metrics = ConcentrationRiskMetrics { portfolio_id: portfolio_id.clone(), total_portfolio_value: total_value, largest_position_pct, largest_position_symbol: Symbol::from( largest_position.base_position.instrument_id.as_str(), ), hhi_index: Price::from_decimal(hhi_index), sector_concentrations, strategy_concentrations, geographic_concentrations, concentration_warnings: warnings, calculated_at: Utc::now(), }; if !metrics.concentration_warnings.is_empty() { warn!( "\u{1f6a8} Concentration risk warnings for portfolio {}: {} violations", portfolio_id, metrics.concentration_warnings.len() ); // Record concentration risk breaches for _ in &metrics.concentration_warnings { RISK_BREACHES_COUNTER.inc(); } } // Update concentration risk metrics if let Ok(hhi_f64) = decimal_to_f64_safe(hhi_index, "HHI index conversion") { CONCENTRATION_RISK_GAUGE.set(hhi_f64); } else { warn!("Failed to convert HHI index to f64 for metrics"); } info!( "\u{2705} Concentration risk calculated for {} - HHI: {:.0}, Largest Position: {:.2}%", portfolio_id, hhi_index, largest_position_pct ); Ok(metrics) } /// Update comprehensive portfolio summary with risk metrics async fn update_portfolio_summary(&self, portfolio_id: &PortfolioId) -> RiskResult<()> { let portfolio_positions: Vec<_> = self .positions .iter() .filter(|entry| &entry.key().0 == portfolio_id) .map(|entry| entry.value().clone()) .collect(); if portfolio_positions.is_empty() { return Ok(()); } // Calculate portfolio totals let total_value_decimal: Decimal = portfolio_positions .iter() .map(|pos| { pos.base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO) }) .sum(); let total_value = Price::from_decimal(total_value_decimal); let unrealized_pnl: Decimal = portfolio_positions .iter() .map(|pos| { pos.base_position .unrealized_pnl .to_decimal() .unwrap_or(Decimal::ZERO) }) .sum(); let realized_pnl: Decimal = portfolio_positions .iter() .map(|pos| { pos.base_position .realized_pnl .to_decimal() .unwrap_or(Decimal::ZERO) }) .sum(); // Calculate top positions let mut top_positions: Vec<_> = portfolio_positions .iter() .map(|pos| TopPosition { symbol: Symbol::from(pos.base_position.instrument_id.as_str()), value: Price::from_decimal( pos.base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO), ), percentage: if total_value > Price::ZERO { let market_val_decimal = pos .base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO); Price::from_decimal((market_val_decimal / total_value_decimal) * Decimal::from(100)) } else { Price::ZERO }, pnl: pos .base_position .unrealized_pnl .to_decimal() .unwrap_or(Decimal::ZERO), }) .collect(); top_positions.sort_by(|a, b| b.value.cmp(&a.value)); top_positions.truncate(10); // Keep top 10 // Calculate sector allocation let mut sector_allocation = HashMap::new(); for position in &portfolio_positions { let sector_value = sector_allocation .entry(position.sector.clone()) .or_insert(Price::ZERO); if let Ok(value) = position.base_position.market_value.to_decimal() { *sector_value += value.into() } else { warn!( "Failed to convert market_value to decimal for position {}", position.base_position.instrument_id ) } } // Calculate concentration metrics let concentration_metrics = self.calculate_concentration_risk(portfolio_id).await?; // Create portfolio summary let summary = PortfolioSummary { portfolio_id: portfolio_id.clone(), total_value, total_positions: portfolio_positions.len(), unrealized_pnl, realized_pnl, daily_pnl: unrealized_pnl + realized_pnl, // Simplified daily P&L concentration_metrics, top_positions, sector_allocation, last_updated: Utc::now(), }; self.portfolio_summaries .insert(portfolio_id.clone(), summary); Ok(()) } /// Get portfolio summary with all risk metrics pub async fn get_portfolio_summary( &self, portfolio_id: &PortfolioId, ) -> Option { self.portfolio_summaries .get(portfolio_id) .map(|entry| entry.clone()) } /// Set concentration limits for a portfolio pub async fn set_concentration_limits( &self, portfolio_id: &PortfolioId, limits: ConcentrationLimits, ) -> RiskResult<()> { let mut limits_map = self.concentration_limits.write().await; limits_map.insert(portfolio_id.clone(), limits); info!( "\u{1f4ca} Concentration limits updated for portfolio {}", portfolio_id ); Ok(()) } /// Get concentration limits for a portfolio pub async fn get_concentration_limits( &self, portfolio_id: &PortfolioId, ) -> RiskResult { let limits_map = self.concentration_limits.read().await; Ok(limits_map.get(portfolio_id).cloned().unwrap_or_default()) } /// Subscribe to position update events #[must_use] pub fn subscribe_to_updates(&self) -> broadcast::Receiver { self.position_update_sender.subscribe() } /// Get all portfolios with positions pub async fn get_active_portfolios(&self) -> Vec { let mut portfolios = Vec::new(); for entry in self.positions.iter() { let portfolio_id = &entry.key().0; if !portfolios.contains(portfolio_id) { portfolios.push(portfolio_id.clone()); } } // Update portfolio count metric PORTFOLIO_COUNT_GAUGE.set(portfolios.len() as i64); portfolios } /// Calculate portfolio beta (systematic risk) pub async fn calculate_portfolio_beta( &self, portfolio_id: &PortfolioId, ) -> RiskResult { let portfolio_positions: Vec<_> = self .positions .iter() .filter(|entry| &entry.key().0 == portfolio_id) .map(|entry| entry.value().clone()) .collect(); if portfolio_positions.is_empty() { return Ok(Decimal::ZERO); } let total_value_decimal: Decimal = portfolio_positions .iter() .map(|pos| { pos.base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO) }) .sum(); let total_value = Price::from_decimal(total_value_decimal); if total_value == Price::ZERO { return Ok(Decimal::ZERO); } // Calculate weighted average beta let weighted_beta: Decimal = portfolio_positions .iter() .map(|pos| { let market_val = pos .base_position .market_value .to_decimal() .unwrap_or(Decimal::ZERO); let weight = if total_value_decimal > Decimal::ZERO { market_val / total_value_decimal } else { Decimal::ZERO }; let beta = pos .beta .map_or(Decimal::ONE, |b| b.to_decimal().unwrap_or(Decimal::ONE)); // Default beta of 1.0 weight * beta }) .sum(); Ok(weighted_beta) } /// Helper methods for classification fn classify_sector(&self, instrument_id: &InstrumentId) -> String { // Simple classification based on symbol (in production, this would use external data) match instrument_id.as_str() { s if s.starts_with("AAPL") || s.starts_with("MSFT") || s.starts_with("GOOGL") => { "Technology".to_owned() } s if s.starts_with("JPM") || s.starts_with("BAC") || s.starts_with("WFC") => { "Financials".to_owned() } s if s.starts_with("JNJ") || s.starts_with("PFE") || s.starts_with("MRK") => { "Healthcare".to_owned() } s if s.contains("USD") || s.contains("EUR") || s.contains("GBP") => { "Currencies".to_owned() } s if s.contains("BTC") || s.contains("ETH") => "Cryptocurrency".to_owned(), _ => "Other".to_owned(), } } fn classify_country(&self, instrument_id: &InstrumentId) -> String { // Simple classification (in production, this would use external data) match instrument_id.as_str() { s if s.contains("USD") => "United States".to_owned(), s if s.contains("EUR") => "European Union".to_owned(), s if s.contains("GBP") => "United Kingdom".to_owned(), s if s.contains("JPY") => "Japan".to_owned(), _ => "United States".to_owned(), // Default for US equities } } fn classify_asset_class(&self, instrument_id: &InstrumentId) -> String { // Simple classification (in production, this would use external data) match instrument_id.as_str() { s if s.contains("USD") || s.contains("EUR") || s.contains("GBP") => { "Currency".to_owned() } s if s.contains("BTC") || s.contains("ETH") => "Cryptocurrency".to_owned(), s if s.contains("BOND") || s.contains("TREASURY") => "Fixed Income".to_owned(), s if s.contains("GOLD") || s.contains("OIL") => "Commodity".to_owned(), _ => "Equity".to_owned(), } } } #[cfg(test)] mod tests { use super::*; // Removed types::operations - using common::types::prelude instead #[tokio::test] async fn test_position_tracking() -> Result<(), Box> { let tracker = PositionTracker::new(); // Create initial position let position = tracker.update_position( "portfolio1".to_string(), "AAPL".to_string(), "strategy1".to_string(), Price::from_f64(100.0)?, Price::from_f64(150.0)?, )?; assert_eq!( position.quantity.to_decimal()?, Decimal::try_from(100.0).map_err(|_| RiskError::CalculationError( "Failed to convert 100.0 to decimal".to_owned() ))? ); assert_eq!( position.position.average_price.to_decimal()?, Price::from_f64(150.0)?.to_decimal()? ); // Add to position let position = tracker.update_position( "portfolio1".to_string(), "AAPL".to_string(), "strategy1".to_string(), Price::from_f64(50.0)?, Price::from_f64(160.0)?, )?; assert_eq!( position.quantity.to_decimal()?, Decimal::try_from(150.0).map_err(|_| RiskError::CalculationError( "Failed to convert 150.0 to decimal".to_owned() ))? ); // Average price should be (100*150 + 50*160) / 150 = 153.33 assert!( position.position.average_price.to_decimal()? > Price::from_f64(153.0)?.to_decimal()? && position.position.average_price.to_decimal()? < Price::from_f64(154.0)?.to_decimal()? ); // Partial close let position = tracker.update_position( "portfolio1".to_string(), "AAPL".to_string(), "strategy1".to_string(), Price::from_f64(-75.0)?, Price::from_f64(155.0)?, )?; assert_eq!( position.quantity.to_decimal()?, Decimal::try_from(75.0).map_err(|_| RiskError::CalculationError( "Failed to convert 75.0 to decimal".to_owned() ))? ); assert!(position.realized_pnl > Price::ZERO); // Should have made profit Ok(()) } #[tokio::test] async fn test_market_data_update() -> Result<(), Box> { let tracker = PositionTracker::new(); // Create position tracker.update_position( "portfolio1".to_string(), "AAPL".to_string(), "strategy1".to_string(), Price::from_f64(100.0)?, Price::from_f64(150.0)?, )?; // Update market data let market_data = MarketData { instrument_id: "AAPL".to_string(), bid: f64_to_price_safe(155.0, "test bid price").unwrap_or(Price::ZERO), ask: f64_to_price_safe(156.0, "test ask price").unwrap_or(Price::ZERO), last_price: f64_to_price_safe(155.0, "test last price").unwrap_or(Price::ZERO), last: f64_to_price_safe(155.0, "test last price").unwrap_or(Price::ZERO), volume: Quantity::from_f64(1000000.0)?, volatility: Some(0.25), // 25% volatility as f64 timestamp: Utc::now().timestamp(), }; tracker.update_market_data(market_data).await?; // Check updated position let position = tracker .get_position(&"portfolio1".to_string()) .await .ok_or("Position not found")?; assert_eq!( position.market_value.to_decimal()?, Price::from_f64(15500.0)?.to_decimal()? ); // 100 * 155 assert_eq!( position.unrealized_pnl.to_decimal()?, Price::from_f64(500.0)?.to_decimal()? ); // 100 * (155 - 150) Ok(()) } }