//! Performance analytics and metrics for backtesting //! //! Provides comprehensive performance analysis including returns, risk metrics, //! drawdown analysis, and statistical measures for strategy evaluation. use std::collections::HashMap; use anyhow::Result; use chrono::{DateTime, Datelike, Duration as ChronoDuration, TimeZone, Utc}; use serde::{Deserialize, Serialize}; use statrs::statistics::Statistics; use tracing::info; use common::Symbol; use rust_decimal::Decimal; use rust_decimal::MathematicalOps; use crate::strategy_tester::{PerformanceSnapshot, TradeRecord}; /// Comprehensive performance analytics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PerformanceAnalytics { /// Basic return metrics pub returns: ReturnMetrics, /// Risk metrics pub risk: RiskMetrics, /// Drawdown analysis pub drawdown: DrawdownMetrics, /// Trade statistics pub trade_stats: TradeStatistics, /// Benchmark comparison pub benchmark: Option, /// Portfolio metrics pub portfolio: PortfolioMetrics, /// Time-based analysis pub time_analysis: TimeAnalysis, } /// Return-based metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReturnMetrics { /// Total return pub total_return: Decimal, /// Annualized return pub annualized_return: Decimal, /// Compound annual growth rate (CAGR) pub cagr: Decimal, /// Daily returns pub daily_returns: Vec, /// Monthly returns pub monthly_returns: Vec, /// Best single day return pub best_day: Decimal, /// Worst single day return pub worst_day: Decimal, /// Average daily return pub avg_daily_return: Decimal, /// Median daily return pub median_daily_return: Decimal, /// Return standard deviation pub return_std: Decimal, /// Skewness of returns pub skewness: Decimal, /// Kurtosis of returns pub kurtosis: Decimal, } /// Risk-based metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RiskMetrics { /// Sharpe ratio pub sharpe_ratio: Decimal, /// Sortino ratio pub sortino_ratio: Decimal, /// Calmar ratio pub calmar_ratio: Decimal, /// Value at Risk (VaR) 95% pub var_95: Decimal, /// Value at Risk (VaR) 99% pub var_99: Decimal, /// Conditional Value at Risk (CVaR) 95% pub cvar_95: Decimal, /// Maximum consecutive losses pub max_consecutive_losses: u32, /// Beta (if benchmark provided) pub beta: Option, /// Alpha (if benchmark provided) pub alpha: Option, /// Tracking error (if benchmark provided) pub tracking_error: Option, /// Information ratio (if benchmark provided) pub information_ratio: Option, } /// Drawdown analysis #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrawdownMetrics { /// Maximum drawdown pub max_drawdown: Decimal, /// Current drawdown pub current_drawdown: Decimal, /// Average drawdown pub avg_drawdown: Decimal, /// Maximum drawdown duration (days) pub max_drawdown_duration: i64, /// Current drawdown duration (days) pub current_drawdown_duration: i64, /// Recovery time from max drawdown (days) pub recovery_time: Option, /// Drawdown periods pub drawdown_periods: Vec, /// Underwater curve pub underwater_curve: Vec<(DateTime, Decimal)>, } /// Individual drawdown period #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrawdownPeriod { /// Start date of drawdown pub start_date: DateTime, /// End date of drawdown pub end_date: Option>, /// Peak value before drawdown pub peak_value: Decimal, /// Trough value during drawdown pub trough_value: Decimal, /// Maximum drawdown during period pub max_drawdown: Decimal, /// Duration in days pub duration: i64, /// Recovery date pub recovery_date: Option>, } /// Trade-based statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TradeStatistics { /// Total number of trades pub total_trades: u64, /// Winning trades pub winning_trades: u64, /// Losing trades pub losing_trades: u64, /// Win rate pub win_rate: Decimal, /// Average trade return pub avg_trade_return: Decimal, /// Average winning trade pub avg_winning_trade: Decimal, /// Average losing trade pub avg_losing_trade: Decimal, /// Best trade return pub best_trade: Decimal, /// Worst trade return pub worst_trade: Decimal, /// Profit factor pub profit_factor: Decimal, /// Average trade duration pub avg_trade_duration: ChronoDuration, /// Trades per symbol pub trades_per_symbol: HashMap, /// Monthly trade count pub monthly_trade_count: Vec<(DateTime, u64)>, } /// Portfolio-level metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PortfolioMetrics { /// Initial capital pub initial_capital: Decimal, /// Final portfolio value pub final_value: Decimal, /// Peak portfolio value pub peak_value: Decimal, /// Average portfolio value pub avg_portfolio_value: Decimal, /// Total fees paid pub total_fees: Decimal, /// Total slippage cost pub total_slippage: Decimal, /// Portfolio turnover pub turnover: Decimal, /// Average number of positions pub avg_positions: Decimal, /// Maximum positions held pub max_positions: u32, /// Cash utilization pub cash_utilization: Decimal, } /// Time-based analysis #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimeAnalysis { /// Strategy start date pub start_date: DateTime, /// Strategy end date pub end_date: DateTime, /// Total days pub total_days: i64, /// Trading days pub trading_days: i64, /// Monthly performance pub monthly_performance: Vec, /// Yearly performance pub yearly_performance: Vec, /// Best month pub best_month: Decimal, /// Worst month pub worst_month: Decimal, /// Best year pub best_year: Decimal, /// Worst year pub worst_year: Decimal, } /// Monthly performance summary #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MonthlyPerformance { /// Month/year pub month: DateTime, /// Return for the month pub return_pct: Decimal, /// Number of trades pub trade_count: u64, /// Win rate for the month pub win_rate: Decimal, /// Portfolio value at month end pub portfolio_value: Decimal, } /// Yearly performance summary #[derive(Debug, Clone, Serialize, Deserialize)] pub struct YearlyPerformance { /// Year pub year: i32, /// Return for the year pub return_pct: Decimal, /// Number of trades pub trade_count: u64, /// Win rate for the year pub win_rate: Decimal, /// Portfolio value at year end pub portfolio_value: Decimal, /// Maximum drawdown during year pub max_drawdown: Decimal, } /// Benchmark comparison metrics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BenchmarkComparison { /// Benchmark name pub benchmark_name: String, /// Benchmark total return pub benchmark_return: Decimal, /// Strategy excess return pub excess_return: Decimal, /// Beta coefficient pub beta: Decimal, /// Alpha (risk-adjusted excess return) pub alpha: Decimal, /// Tracking error pub tracking_error: Decimal, /// Information ratio pub information_ratio: Decimal, /// Up capture ratio pub up_capture: Decimal, /// Down capture ratio pub down_capture: Decimal, } /// Performance metrics calculator pub struct MetricsCalculator { /// Performance snapshots snapshots: Vec, /// Trade records trades: Vec, /// Benchmark data (if available) benchmark_data: Option, Decimal)>>, /// Risk-free rate (annualized) risk_free_rate: Decimal, } impl MetricsCalculator { /// Create new metrics calculator /// /// # Arguments /// * `risk_free_rate` - Annual risk-free rate for Sharpe ratio calculations pub fn new(risk_free_rate: Decimal) -> Self { Self { snapshots: Vec::new(), trades: Vec::new(), benchmark_data: None, risk_free_rate, } } /// Add performance snapshot /// /// # Arguments /// * `snapshot` - Performance snapshot to add to the collection pub fn add_snapshot(&mut self, snapshot: PerformanceSnapshot) { self.snapshots.push(snapshot); } /// Add trade record /// /// # Arguments /// * `trade` - Trade record to add to the collection pub fn add_trade(&mut self, trade: TradeRecord) { self.trades.push(trade); } /// Set benchmark data /// /// # Arguments /// * `benchmark_name` - Name of the benchmark for identification /// * `data` - Time series data of benchmark values as (timestamp, value) pairs pub fn set_benchmark(&mut self, _benchmark_name: String, data: Vec<(DateTime, Decimal)>) { self.benchmark_data = Some(data); } /// Calculate comprehensive performance analytics pub fn calculate_analytics(&self) -> Result { if self.snapshots.is_empty() { return Err(anyhow::anyhow!("No performance snapshots available")); } info!( "Calculating performance analytics for {} snapshots and {} trades", self.snapshots.len(), self.trades.len() ); let returns = self.calculate_return_metrics()?; let risk = self.calculate_risk_metrics(&returns)?; let drawdown = self.calculate_drawdown_metrics()?; let trade_stats = self.calculate_trade_statistics()?; let benchmark = self.calculate_benchmark_comparison(&returns)?; let portfolio = self.calculate_portfolio_metrics()?; let time_analysis = self.calculate_time_analysis()?; Ok(PerformanceAnalytics { returns, risk, drawdown, trade_stats, benchmark, portfolio, time_analysis, }) } /// Calculate return-based metrics /// /// # Returns /// * `Result` - Comprehensive return metrics including total return, volatility, and skewness fn calculate_return_metrics(&self) -> Result { let daily_returns = self.calculate_daily_returns()?; if daily_returns.is_empty() { return Err(anyhow::anyhow!("No daily returns calculated")); } let total_return = self.calculate_total_return()?; let annualized_return = self.calculate_annualized_return(&daily_returns)?; let cagr = self.calculate_cagr()?; let returns_f64: Vec = daily_returns .iter() .map(|d| d.to_string().parse().unwrap_or(0.0)) .collect(); let avg_daily_return = if !returns_f64.is_empty() { Decimal::from_f64_retain(returns_f64.clone().mean()).unwrap_or_default() } else { Decimal::ZERO }; let return_std = if returns_f64.len() > 1 { Decimal::from_f64_retain(returns_f64.clone().std_dev()).unwrap_or_default() } else { Decimal::ZERO }; let best_day = daily_returns.iter().max().cloned().unwrap_or_default(); let worst_day = daily_returns.iter().min().cloned().unwrap_or_default(); // Calculate median let mut sorted_returns = daily_returns.clone(); sorted_returns.sort(); let median_daily_return = if !sorted_returns.is_empty() { if sorted_returns.len() % 2 == 0 { let mid = sorted_returns.len() / 2; (sorted_returns[mid - 1] + sorted_returns[mid]) / Decimal::from(2) } else { sorted_returns[sorted_returns.len() / 2] } } else { Decimal::ZERO }; // Calculate skewness and kurtosis (simplified) let skewness = self.calculate_skewness(&returns_f64); let kurtosis = self.calculate_kurtosis(&returns_f64); let monthly_returns = self.calculate_monthly_returns()?; Ok(ReturnMetrics { total_return, annualized_return, cagr, daily_returns, monthly_returns, best_day, worst_day, avg_daily_return, median_daily_return, return_std, skewness, kurtosis, }) } /// Calculate risk metrics /// /// # Arguments /// * `returns` - Return metrics used for risk calculations /// /// # Returns /// * `Result` - Risk-adjusted metrics including Sharpe ratio, VaR, and drawdown analysis fn calculate_risk_metrics(&self, returns: &ReturnMetrics) -> Result { let sharpe_ratio = self.calculate_sharpe_ratio(&returns.daily_returns)?; let sortino_ratio = self.calculate_sortino_ratio(&returns.daily_returns)?; let calmar_ratio = self.calculate_calmar_ratio(returns.annualized_return)?; let (var_95, var_99) = self.calculate_var(&returns.daily_returns)?; let cvar_95 = self.calculate_cvar(&returns.daily_returns, Decimal::new(5, 2))?; let max_consecutive_losses = self.calculate_max_consecutive_losses()?; // Benchmark-related metrics let (beta, alpha, tracking_error, information_ratio) = if self.benchmark_data.is_some() { self.calculate_benchmark_risk_metrics(&returns.daily_returns)? } else { (None, None, None, None) }; Ok(RiskMetrics { sharpe_ratio, sortino_ratio, calmar_ratio, var_95, var_99, cvar_95, max_consecutive_losses, beta, alpha, tracking_error, information_ratio, }) } /// Calculate drawdown metrics /// /// # Returns /// * `Result` - Comprehensive drawdown analysis including maximum drawdown and recovery times fn calculate_drawdown_metrics(&self) -> Result { let (max_drawdown, current_drawdown, drawdown_periods, underwater_curve) = self.calculate_drawdowns()?; let avg_drawdown = if !drawdown_periods.is_empty() { let sum: Decimal = drawdown_periods.iter().map(|p| p.max_drawdown).sum(); sum / Decimal::from(drawdown_periods.len()) } else { Decimal::ZERO }; let max_drawdown_duration = drawdown_periods .iter() .map(|p| p.duration) .max() .unwrap_or(0); let current_drawdown_duration = if let Some(period) = drawdown_periods.last() { if period.end_date.is_none() { period.duration } else { 0 } } else { 0 }; let recovery_time = drawdown_periods .iter() .find(|p| p.max_drawdown == max_drawdown) .and_then(|p| p.recovery_date) .map(|recovery| { if let Some(peak_period) = drawdown_periods .iter() .find(|pp| pp.max_drawdown == max_drawdown) { (recovery - peak_period.start_date).num_days() } else { 0 } }); Ok(DrawdownMetrics { max_drawdown, current_drawdown, avg_drawdown, max_drawdown_duration, current_drawdown_duration, recovery_time, drawdown_periods, underwater_curve, }) } /// Calculate trade statistics /// /// # Returns /// * `Result` - Detailed trade-level statistics including win rate and profit factor fn calculate_trade_statistics(&self) -> Result { if self.trades.is_empty() { return Ok(TradeStatistics { total_trades: 0, winning_trades: 0, losing_trades: 0, win_rate: Decimal::ZERO, avg_trade_return: Decimal::ZERO, avg_winning_trade: Decimal::ZERO, avg_losing_trade: Decimal::ZERO, best_trade: Decimal::ZERO, worst_trade: Decimal::ZERO, profit_factor: Decimal::ZERO, avg_trade_duration: ChronoDuration::zero(), trades_per_symbol: HashMap::new(), monthly_trade_count: Vec::new(), }); } let total_trades = self.trades.len() as u64; let winning_trades = self .trades .iter() .filter(|t| t.return_pct > Decimal::ZERO) .count() as u64; let losing_trades = total_trades - winning_trades; let win_rate = if total_trades > 0 { Decimal::from(winning_trades) / Decimal::from(total_trades) } else { Decimal::ZERO }; let avg_trade_return = if !self.trades.is_empty() { let sum: Decimal = self.trades.iter().map(|t| t.return_pct).sum(); sum / Decimal::from(self.trades.len()) } else { Decimal::ZERO }; let winning_trades_vec: Vec<_> = self .trades .iter() .filter(|t| t.return_pct > Decimal::ZERO) .collect(); let losing_trades_vec: Vec<_> = self .trades .iter() .filter(|t| t.return_pct <= Decimal::ZERO) .collect(); let avg_winning_trade = if !winning_trades_vec.is_empty() { let sum: Decimal = winning_trades_vec.iter().map(|t| t.return_pct).sum(); sum / Decimal::from(winning_trades_vec.len()) } else { Decimal::ZERO }; let avg_losing_trade = if !losing_trades_vec.is_empty() { let sum: Decimal = losing_trades_vec.iter().map(|t| t.return_pct).sum(); sum / Decimal::from(losing_trades_vec.len()) } else { Decimal::ZERO }; let best_trade = self .trades .iter() .map(|t| t.return_pct) .max() .unwrap_or_default(); let worst_trade = self .trades .iter() .map(|t| t.return_pct) .min() .unwrap_or_default(); let gross_profit: Decimal = winning_trades_vec.iter().map(|t| t.pnl).sum(); let gross_loss: Decimal = losing_trades_vec.iter().map(|t| t.pnl.abs()).sum(); let profit_factor = if gross_loss > Decimal::ZERO { gross_profit / gross_loss } else { Decimal::ZERO }; let avg_trade_duration = if !self.trades.is_empty() { let total_duration: i64 = self .trades .iter() .map(|t| (t.exit_time - t.entry_time).num_seconds()) .sum(); ChronoDuration::seconds(total_duration / self.trades.len() as i64) } else { ChronoDuration::zero() }; // Calculate trades per symbol let mut trades_per_symbol = HashMap::new(); for trade in &self.trades { *trades_per_symbol.entry(trade.symbol.clone()).or_insert(0) += 1; } // Calculate monthly trade count let monthly_trade_count = self.calculate_monthly_trade_count(); Ok(TradeStatistics { total_trades, winning_trades, losing_trades, win_rate, avg_trade_return, avg_winning_trade, avg_losing_trade, best_trade, worst_trade, profit_factor, avg_trade_duration, trades_per_symbol, monthly_trade_count, }) } /// Calculate benchmark comparison if available /// /// # Arguments /// * `returns` - Return metrics to compare against benchmark /// /// # Returns /// * `Result>` - Benchmark comparison metrics if benchmark data is available fn calculate_benchmark_comparison( &self, returns: &ReturnMetrics, ) -> Result> { let benchmark_data = match &self.benchmark_data { Some(data) if !data.is_empty() => data, _ => return Ok(None), }; // Extract benchmark name let benchmark_name = "benchmark".to_string(); // Default name // Calculate benchmark returns let mut benchmark_returns = Vec::new(); for i in 1..benchmark_data.len() { let prev_value = benchmark_data[i - 1].1; let curr_value = benchmark_data[i].1; if prev_value > Decimal::ZERO { let return_pct = (curr_value - prev_value) / prev_value; benchmark_returns.push(return_pct); } } if benchmark_returns.is_empty() { return Ok(None); } // Calculate strategy daily returns let strategy_returns = self.calculate_daily_returns()?; if strategy_returns.is_empty() { return Ok(None); } // Align returns (use minimum length) let min_len = strategy_returns.len().min(benchmark_returns.len()); let strategy_returns = &strategy_returns[..min_len]; let benchmark_returns = &benchmark_returns[..min_len]; // Calculate benchmark total return let benchmark_return = benchmark_returns.iter().sum::(); // Calculate excess return let excess_return = returns.total_return - benchmark_return; // Calculate beta (covariance / variance) let strategy_mean = strategy_returns.iter().sum::() / Decimal::from(strategy_returns.len()); let benchmark_mean = benchmark_returns.iter().sum::() / Decimal::from(benchmark_returns.len()); let mut covariance = Decimal::ZERO; let mut benchmark_variance = Decimal::ZERO; for i in 0..min_len { let strategy_dev = strategy_returns[i] - strategy_mean; let benchmark_dev = benchmark_returns[i] - benchmark_mean; covariance += strategy_dev * benchmark_dev; benchmark_variance += benchmark_dev * benchmark_dev; } covariance /= Decimal::from(min_len); benchmark_variance /= Decimal::from(min_len); let beta = if benchmark_variance > Decimal::ZERO { covariance / benchmark_variance } else { Decimal::ZERO }; // Calculate alpha (CAPM formula) // Alpha = Strategy Return - (Risk-free Rate + Beta * (Benchmark Return - Risk-free Rate)) let alpha = returns.annualized_return - (self.risk_free_rate + beta * (benchmark_return - self.risk_free_rate)); // Calculate tracking error (std dev of excess returns) let mut excess_returns = Vec::new(); for i in 0..min_len { excess_returns.push(strategy_returns[i] - benchmark_returns[i]); } let excess_mean = excess_returns.iter().sum::() / Decimal::from(excess_returns.len()); let mut tracking_variance = Decimal::ZERO; for excess_return in &excess_returns { let dev = excess_return - excess_mean; tracking_variance += dev * dev; } tracking_variance /= Decimal::from(excess_returns.len()); let tracking_error = tracking_variance.sqrt().unwrap_or(Decimal::ZERO); // Calculate information ratio let information_ratio = if tracking_error > Decimal::ZERO { alpha / tracking_error } else { Decimal::ZERO }; // Calculate up/down capture ratios let mut up_strategy = Vec::new(); let mut up_benchmark = Vec::new(); let mut down_strategy = Vec::new(); let mut down_benchmark = Vec::new(); for i in 0..min_len { if benchmark_returns[i] > Decimal::ZERO { up_strategy.push(strategy_returns[i]); up_benchmark.push(benchmark_returns[i]); } else if benchmark_returns[i] < Decimal::ZERO { down_strategy.push(strategy_returns[i]); down_benchmark.push(benchmark_returns[i]); } } let up_capture = if !up_benchmark.is_empty() { let up_strategy_avg = up_strategy.iter().sum::() / Decimal::from(up_strategy.len()); let up_benchmark_avg = up_benchmark.iter().sum::() / Decimal::from(up_benchmark.len()); if up_benchmark_avg > Decimal::ZERO { up_strategy_avg / up_benchmark_avg } else { Decimal::ZERO } } else { Decimal::ZERO }; let down_capture = if !down_benchmark.is_empty() { let down_strategy_avg = down_strategy.iter().sum::() / Decimal::from(down_strategy.len()); let down_benchmark_avg = down_benchmark.iter().sum::() / Decimal::from(down_benchmark.len()); if down_benchmark_avg < Decimal::ZERO { down_strategy_avg / down_benchmark_avg } else { Decimal::ZERO } } else { Decimal::ZERO }; Ok(Some(BenchmarkComparison { benchmark_name, benchmark_return, excess_return, beta, alpha, tracking_error, information_ratio, up_capture, down_capture, })) } /// Calculate portfolio metrics /// /// # Returns /// * `Result` - Portfolio-level metrics including turnover and utilization fn calculate_portfolio_metrics(&self) -> Result { if self.snapshots.is_empty() { return Err(anyhow::anyhow!( "No snapshots available for portfolio metrics" )); } let initial_capital = self.snapshots[0].portfolio_value; let final_value = self .snapshots .last() .ok_or_else(|| anyhow::anyhow!("No snapshots available for final value calculation"))? .portfolio_value; let peak_value = self .snapshots .iter() .map(|s| s.portfolio_value) .max() .unwrap_or(initial_capital); let avg_portfolio_value = if !self.snapshots.is_empty() { let sum: Decimal = self.snapshots.iter().map(|s| s.portfolio_value).sum(); sum / Decimal::from(self.snapshots.len()) } else { initial_capital }; let total_fees = self.trades.iter().map(|t| t.commission).sum(); let total_slippage = Decimal::ZERO; // Would be calculated from execution data // Portfolio turnover calculation (simplified) let turnover = if !self.trades.is_empty() && avg_portfolio_value > Decimal::ZERO { let total_traded: Decimal = self .trades .iter() .map(|t| { t.quantity.to_decimal().unwrap_or_default() * t.entry_price.to_decimal().unwrap_or_default() }) .sum(); total_traded / avg_portfolio_value } else { Decimal::ZERO }; let avg_positions = if !self.snapshots.is_empty() { let sum = self.snapshots.iter().map(|s| s.open_positions).sum::(); Decimal::from(sum) / Decimal::from(self.snapshots.len()) } else { Decimal::ZERO }; let max_positions = self .snapshots .iter() .map(|s| s.open_positions) .max() .unwrap_or(0); let cash_utilization = if initial_capital > Decimal::ZERO { let final_cash = self .snapshots .last() .ok_or_else(|| { anyhow::anyhow!("No snapshots available for cash utilization calculation") })? .cash_balance; (initial_capital - final_cash) / initial_capital } else { Decimal::ZERO }; Ok(PortfolioMetrics { initial_capital, final_value, peak_value, avg_portfolio_value, total_fees, total_slippage, turnover, avg_positions, max_positions, cash_utilization, }) } /// Calculate time-based analysis /// /// # Returns /// * `Result` - Time-based performance analysis including monthly and yearly breakdowns fn calculate_time_analysis(&self) -> Result { if self.snapshots.is_empty() { return Err(anyhow::anyhow!("No snapshots available for time analysis")); } let start_date = self.snapshots[0].timestamp; let end_date = self .snapshots .last() .ok_or_else(|| anyhow::anyhow!("No snapshots available for time analysis end date"))? .timestamp; let total_days = (end_date - start_date).num_days(); let trading_days = self.snapshots.len() as i64; // Simplified let monthly_performance = self.calculate_monthly_performance()?; let yearly_performance = self.calculate_yearly_performance()?; let best_month = monthly_performance .iter() .map(|m| m.return_pct) .max() .unwrap_or_default(); let worst_month = monthly_performance .iter() .map(|m| m.return_pct) .min() .unwrap_or_default(); let best_year = yearly_performance .iter() .map(|y| y.return_pct) .max() .unwrap_or_default(); let worst_year = yearly_performance .iter() .map(|y| y.return_pct) .min() .unwrap_or_default(); Ok(TimeAnalysis { start_date, end_date, total_days, trading_days, monthly_performance, yearly_performance, best_month, worst_month, best_year, worst_year, }) } // Helper methods for calculations /// Calculate daily returns from portfolio snapshots /// /// # Returns /// * `Result>` - Vector of daily return percentages fn calculate_daily_returns(&self) -> Result> { if self.snapshots.len() < 2 { return Ok(Vec::new()); } let mut returns = Vec::new(); for i in 1..self.snapshots.len() { let prev_value = self.snapshots[i - 1].portfolio_value; let curr_value = self.snapshots[i].portfolio_value; if prev_value > Decimal::ZERO { let return_pct = (curr_value - prev_value) / prev_value; returns.push(return_pct); } } Ok(returns) } /// Calculate total return over the entire period /// /// # Returns /// * `Result` - Total return as a percentage fn calculate_total_return(&self) -> Result { if self.snapshots.is_empty() { return Ok(Decimal::ZERO); } let initial_value = self.snapshots[0].portfolio_value; let final_value = self .snapshots .last() .ok_or_else(|| anyhow::anyhow!("No snapshots available for total return calculation"))? .portfolio_value; if initial_value > Decimal::ZERO { Ok((final_value - initial_value) / initial_value) } else { Ok(Decimal::ZERO) } } /// Calculate annualized return from daily returns /// /// # Arguments /// * `daily_returns` - Vector of daily return percentages /// /// # Returns /// * `Result` - Annualized return percentage fn calculate_annualized_return(&self, daily_returns: &[Decimal]) -> Result { if daily_returns.is_empty() { return Ok(Decimal::ZERO); } // Compound daily returns to get annualized return let compound_return = daily_returns .iter() .fold(Decimal::from(1), |acc, &ret| acc * (Decimal::from(1) + ret)); let days = daily_returns.len() as f64; let years = days / 365.25; if years > 0.0 && compound_return > Decimal::ZERO { let annualized = compound_return.powf(1.0 / years) - Decimal::from(1); Ok(annualized) } else { Ok(Decimal::ZERO) } } /// Calculate Compound Annual Growth Rate (CAGR) /// /// # Returns /// * `Result` - CAGR as a percentage fn calculate_cagr(&self) -> Result { if self.snapshots.len() < 2 { return Ok(Decimal::ZERO); } let initial_value = self.snapshots[0].portfolio_value; let final_value = self .snapshots .last() .ok_or_else(|| anyhow::anyhow!("No snapshots available for CAGR calculation"))? .portfolio_value; let start_date = self.snapshots[0].timestamp; let end_date = self .snapshots .last() .ok_or_else(|| anyhow::anyhow!("No snapshots available for CAGR end date"))? .timestamp; let years = (end_date - start_date).num_days() as f64 / 365.25; if years > 0.0 && initial_value > Decimal::ZERO && final_value > Decimal::ZERO { let cagr = (final_value / initial_value).powf(1.0 / years) - Decimal::from(1); Ok(cagr) } else { Ok(Decimal::ZERO) } } /// Calculate Sharpe ratio from daily returns /// /// # Arguments /// * `daily_returns` - Vector of daily return percentages /// /// # Returns /// * `Result` - Annualized Sharpe ratio fn calculate_sharpe_ratio(&self, daily_returns: &[Decimal]) -> Result { if daily_returns.is_empty() { return Ok(Decimal::ZERO); } let returns_f64: Vec = daily_returns .iter() .map(|d| d.to_string().parse().unwrap_or(0.0)) .collect(); let mean_return = returns_f64.clone().mean(); let std_dev = if returns_f64.len() > 1 { returns_f64.clone().std_dev() } else { return Ok(Decimal::ZERO); }; let daily_risk_free = (self.risk_free_rate / Decimal::from(365)) .to_string() .parse::() .unwrap_or(0.0); let excess_return = mean_return - daily_risk_free; if std_dev > 0.0 { let sharpe = excess_return / std_dev; let annualized_sharpe = sharpe * (365.25_f64).sqrt(); Ok(Decimal::from_f64_retain(annualized_sharpe).unwrap_or_default()) } else { Ok(Decimal::ZERO) } } /// Calculate Sortino ratio focusing on downside risk /// /// # Arguments /// * `daily_returns` - Vector of daily return percentages /// /// # Returns /// * `Result` - Annualized Sortino ratio fn calculate_sortino_ratio(&self, daily_returns: &[Decimal]) -> Result { if daily_returns.is_empty() { return Ok(Decimal::ZERO); } let returns_f64: Vec = daily_returns .iter() .map(|d| d.to_string().parse().unwrap_or(0.0)) .collect(); let mean_return = returns_f64.clone().mean(); let daily_risk_free = (self.risk_free_rate / Decimal::from(365)) .to_string() .parse::() .unwrap_or(0.0); // Calculate downside deviation let negative_returns: Vec = returns_f64 .iter() .filter(|&&r| r < daily_risk_free) .map(|&r| (r - daily_risk_free).powi(2)) .collect(); if negative_returns.is_empty() { return Ok(Decimal::ZERO); } let downside_deviation = (negative_returns.iter().sum::() / negative_returns.len() as f64).sqrt(); if downside_deviation > 0.0 { let sortino = (mean_return - daily_risk_free) / downside_deviation; let annualized_sortino = sortino * (365.25_f64).sqrt(); Ok(Decimal::from_f64_retain(annualized_sortino).unwrap_or_default()) } else { Ok(Decimal::ZERO) } } /// Calculate Calmar ratio (return to maximum drawdown) /// /// # Arguments /// * `annualized_return` - Annualized return percentage /// /// # Returns /// * `Result` - Calmar ratio fn calculate_calmar_ratio(&self, annualized_return: Decimal) -> Result { let max_drawdown = self.calculate_max_drawdown()?; if max_drawdown.abs() > Decimal::ZERO { Ok(annualized_return / max_drawdown.abs()) } else { Ok(Decimal::ZERO) } } /// Calculate maximum drawdown from portfolio snapshots /// /// # Returns /// * `Result` - Maximum drawdown as a negative percentage fn calculate_max_drawdown(&self) -> Result { if self.snapshots.is_empty() { return Ok(Decimal::ZERO); } let mut max_dd = Decimal::ZERO; let mut peak = self.snapshots[0].portfolio_value; for snapshot in &self.snapshots { if snapshot.portfolio_value > peak { peak = snapshot.portfolio_value; } let drawdown = (snapshot.portfolio_value - peak) / peak; if drawdown < max_dd { max_dd = drawdown; } } Ok(max_dd) } /// Calculate Value at Risk (VaR) at 95% and 99% confidence levels /// /// # Arguments /// * `daily_returns` - Vector of daily return percentages /// /// # Returns /// * `Result<(Decimal, Decimal)>` - Tuple of (VaR 95%, VaR 99%) fn calculate_var(&self, daily_returns: &[Decimal]) -> Result<(Decimal, Decimal)> { if daily_returns.is_empty() { return Ok((Decimal::ZERO, Decimal::ZERO)); } let mut sorted_returns = daily_returns.to_vec(); sorted_returns.sort(); let var_95_idx = (sorted_returns.len() as f64 * 0.05) as usize; let var_99_idx = (sorted_returns.len() as f64 * 0.01) as usize; let var_95 = if var_95_idx < sorted_returns.len() { sorted_returns[var_95_idx] } else { Decimal::ZERO }; let var_99 = if var_99_idx < sorted_returns.len() { sorted_returns[var_99_idx] } else { Decimal::ZERO }; Ok((var_95, var_99)) } /// Calculate Conditional Value at Risk (CVaR) /// /// # Arguments /// * `daily_returns` - Vector of daily return percentages /// * `confidence_level` - Confidence level (e.g., 0.05 for 95% confidence) /// /// # Returns /// * `Result` - CVaR value fn calculate_cvar( &self, daily_returns: &[Decimal], confidence_level: Decimal, ) -> Result { if daily_returns.is_empty() { return Ok(Decimal::ZERO); } let mut sorted_returns = daily_returns.to_vec(); sorted_returns.sort(); let cutoff_idx = (sorted_returns.len() as f64 * confidence_level.to_string().parse::().unwrap_or(0.05)) as usize; if cutoff_idx == 0 { return Ok(Decimal::ZERO); } let tail_returns = &sorted_returns[..cutoff_idx]; if tail_returns.is_empty() { return Ok(Decimal::ZERO); } let cvar = tail_returns.iter().sum::() / Decimal::from(tail_returns.len()); Ok(cvar) } /// Calculate maximum consecutive losing trades /// /// # Returns /// * `Result` - Maximum number of consecutive losses fn calculate_max_consecutive_losses(&self) -> Result { let mut max_consecutive = 0; let mut current_consecutive = 0; for trade in &self.trades { if trade.return_pct < Decimal::ZERO { current_consecutive += 1; max_consecutive = max_consecutive.max(current_consecutive); } else { current_consecutive = 0; } } Ok(max_consecutive) } /// Calculate risk metrics relative to benchmark /// /// # Arguments /// * `_daily_returns` - Vector of daily return percentages (currently unused) /// /// # Returns /// * `Result<(Option, Option, Option, Option)>` - /// Tuple of (beta, alpha, tracking_error, information_ratio) fn calculate_benchmark_risk_metrics( &self, _daily_returns: &[Decimal], ) -> Result<( Option, Option, Option, Option, )> { // Implementation for benchmark risk metrics calculation Ok((None, None, None, None)) } /// Calculate comprehensive drawdown analysis /// /// # Returns /// * `Result<(Decimal, Decimal, Vec, Vec<(DateTime, Decimal)>)>` - /// Tuple of (max_drawdown, current_drawdown, drawdown_periods, underwater_curve) fn calculate_drawdowns( &self, ) -> Result<( Decimal, Decimal, Vec, Vec<(DateTime, Decimal)>, )> { if self.snapshots.is_empty() { return Ok((Decimal::ZERO, Decimal::ZERO, Vec::new(), Vec::new())); } let mut max_drawdown = Decimal::ZERO; let mut peak = self.snapshots[0].portfolio_value; let mut drawdown_periods = Vec::new(); let mut underwater_curve = Vec::new(); let mut in_drawdown = false; let mut drawdown_start: Option> = None; let mut drawdown_peak = Decimal::ZERO; for snapshot in &self.snapshots { if snapshot.portfolio_value > peak { // New peak - end any current drawdown if in_drawdown { if let Some(start) = drawdown_start { drawdown_periods.push(DrawdownPeriod { start_date: start, end_date: Some(snapshot.timestamp), peak_value: drawdown_peak, trough_value: peak, // This would be the actual trough max_drawdown: (peak - drawdown_peak) / drawdown_peak, duration: (snapshot.timestamp - start).num_days(), recovery_date: Some(snapshot.timestamp), }); } in_drawdown = false; } peak = snapshot.portfolio_value; } let current_drawdown = (snapshot.portfolio_value - peak) / peak; underwater_curve.push((snapshot.timestamp, current_drawdown)); if current_drawdown < Decimal::ZERO && !in_drawdown { // Start of new drawdown in_drawdown = true; drawdown_start = Some(snapshot.timestamp); drawdown_peak = peak; } if current_drawdown < max_drawdown { max_drawdown = current_drawdown; } } // Handle ongoing drawdown if in_drawdown { if let Some(start) = drawdown_start { drawdown_periods.push(DrawdownPeriod { start_date: start, end_date: None, peak_value: drawdown_peak, trough_value: self .snapshots .last() .map(|s| s.portfolio_value) .unwrap_or(drawdown_peak), max_drawdown: self .snapshots .last() .map(|s| (s.portfolio_value - drawdown_peak) / drawdown_peak) .unwrap_or(Decimal::ZERO), duration: self .snapshots .last() .map(|s| (s.timestamp - start).num_days()) .unwrap_or(0), recovery_date: None, }); } } let current_drawdown = if let Some(last_snapshot) = self.snapshots.last() { (last_snapshot.portfolio_value - peak) / peak } else { Decimal::ZERO }; Ok(( max_drawdown, current_drawdown, drawdown_periods, underwater_curve, )) } /// Calculate monthly return percentages /// /// # Returns /// * `Result>` - Vector of monthly returns fn calculate_monthly_returns(&self) -> Result> { // Implementation for monthly returns calculation Ok(Vec::new()) } /// Calculate number of trades per month /// /// # Returns /// * `Vec<(DateTime, u64)>` - Vector of (month, trade_count) pairs fn calculate_monthly_trade_count(&self) -> Vec<(DateTime, u64)> { // Implementation for monthly trade count calculation Vec::new() } /// Calculate detailed monthly performance metrics /// /// # Returns /// * `Result>` - Vector of monthly performance summaries fn calculate_monthly_performance(&self) -> Result> { use std::collections::BTreeMap; if self.snapshots.is_empty() { return Ok(Vec::new()); } // Group snapshots by month let mut monthly_groups: BTreeMap<(i32, u32), Vec<&PerformanceSnapshot>> = BTreeMap::new(); for snapshot in &self.snapshots { let key = (snapshot.timestamp.year(), snapshot.timestamp.month()); monthly_groups.entry(key).or_default().push(snapshot); } // Calculate metrics for each month let mut monthly_performance = Vec::new(); for ((year, month), snapshots) in monthly_groups { if snapshots.is_empty() { continue; } let start_value = snapshots.first().unwrap().portfolio_value; let end_value = snapshots.last().unwrap().portfolio_value; let return_pct = if start_value > Decimal::ZERO { ((end_value - start_value) / start_value) * Decimal::from(100) } else { Decimal::ZERO }; // Count trades in this month let trade_count = self.trades.iter() .filter(|t| { let trade_month = (t.exit_time.year(), t.exit_time.month()); trade_month == (year, month) }) .count() as u64; // Calculate win rate for month let month_trades: Vec<_> = self.trades.iter() .filter(|t| { let trade_month = (t.exit_time.year(), t.exit_time.month()); trade_month == (year, month) }) .collect(); let winning_trades = month_trades.iter() .filter(|t| t.pnl > Decimal::ZERO) .count(); let win_rate = if !month_trades.is_empty() { Decimal::from(winning_trades) / Decimal::from(month_trades.len()) } else { Decimal::ZERO }; // Use first day of month for timestamp let month_timestamp = chrono::Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0) .single() .unwrap_or_else(|| snapshots.first().unwrap().timestamp); monthly_performance.push(MonthlyPerformance { month: month_timestamp, return_pct, trade_count, win_rate, portfolio_value: end_value, }); } Ok(monthly_performance) } /// Calculate detailed yearly performance metrics /// /// # Returns /// * `Result>` - Vector of yearly performance summaries fn calculate_yearly_performance(&self) -> Result> { use std::collections::BTreeMap; if self.snapshots.is_empty() { return Ok(Vec::new()); } // Group snapshots by year let mut yearly_groups: BTreeMap> = BTreeMap::new(); for snapshot in &self.snapshots { yearly_groups.entry(snapshot.timestamp.year()).or_default().push(snapshot); } // Calculate metrics for each year let mut yearly_performance = Vec::new(); for (year, snapshots) in yearly_groups { if snapshots.is_empty() { continue; } let start_value = snapshots.first().unwrap().portfolio_value; let end_value = snapshots.last().unwrap().portfolio_value; let return_pct = if start_value > Decimal::ZERO { ((end_value - start_value) / start_value) * Decimal::from(100) } else { Decimal::ZERO }; // Count trades in this year let trade_count = self.trades.iter() .filter(|t| t.exit_time.year() == year) .count() as u64; // Calculate win rate for year let year_trades: Vec<_> = self.trades.iter() .filter(|t| t.exit_time.year() == year) .collect(); let winning_trades = year_trades.iter() .filter(|t| t.pnl > Decimal::ZERO) .count(); let win_rate = if !year_trades.is_empty() { Decimal::from(winning_trades) / Decimal::from(year_trades.len()) } else { Decimal::ZERO }; // Calculate max drawdown for this year let year_snapshots: Vec<_> = snapshots.clone(); let mut peak = year_snapshots[0].portfolio_value; let mut max_drawdown = Decimal::ZERO; for snapshot in &year_snapshots { if snapshot.portfolio_value > peak { peak = snapshot.portfolio_value; } let drawdown = if peak > Decimal::ZERO { ((snapshot.portfolio_value - peak) / peak) * Decimal::from(100) } else { Decimal::ZERO }; if drawdown < max_drawdown { max_drawdown = drawdown; } } yearly_performance.push(YearlyPerformance { year, return_pct, trade_count, win_rate, portfolio_value: end_value, max_drawdown, }); } Ok(yearly_performance) } /// Calculate skewness of returns distribution /// /// # Arguments /// * `returns` - Vector of return values as f64 /// /// # Returns /// * `Decimal` - Skewness measure (0 = symmetric, positive = right tail, negative = left tail) fn calculate_skewness(&self, returns: &[f64]) -> Decimal { if returns.len() < 3 { return Decimal::ZERO; } let mean = returns.mean(); let std_dev = returns.std_dev(); if std_dev == 0.0 { return Decimal::ZERO; } let n = returns.len() as f64; let skewness = returns .iter() .map(|&x| ((x - mean) / std_dev).powi(3)) .sum::() * n / ((n - 1.0) * (n - 2.0)); Decimal::from_f64_retain(skewness).unwrap_or_default() } /// Calculate kurtosis of returns distribution /// /// # Arguments /// * `returns` - Vector of return values as f64 /// /// # Returns /// * `Decimal` - Excess kurtosis measure (0 = normal, positive = fat tails, negative = thin tails) fn calculate_kurtosis(&self, returns: &[f64]) -> Decimal { if returns.len() < 4 { return Decimal::ZERO; } let mean = returns.mean(); let std_dev = returns.std_dev(); if std_dev == 0.0 { return Decimal::ZERO; } let n = returns.len() as f64; let kurtosis = returns .iter() .map(|&x| ((x - mean) / std_dev).powi(4)) .sum::() * n * (n + 1.0) / ((n - 1.0) * (n - 2.0) * (n - 3.0)) - 3.0 * (n - 1.0) * (n - 1.0) / ((n - 2.0) * (n - 3.0)); Decimal::from_f64_retain(kurtosis).unwrap_or_default() } } // Extension trait for Decimal power operations /// Extension trait providing power operations for Decimal type trait DecimalPower { /// Raise Decimal to a floating-point power /// /// # Arguments /// * `exp` - Exponent as f64 /// /// # Returns /// * `Decimal` - Result of self^exp fn powf(self, exp: f64) -> Decimal; } impl DecimalPower for Decimal { fn powf(self, exp: f64) -> Decimal { let base_f64 = self.to_string().parse::().unwrap_or(0.0); let result = base_f64.powf(exp); Decimal::from_f64_retain(result).unwrap_or_default() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_metrics_calculator_creation() { let calculator = MetricsCalculator::new(Decimal::new(2, 2)); // 2% risk-free rate assert_eq!(calculator.risk_free_rate, Decimal::new(2, 2)); } #[test] fn test_empty_calculations() { let calculator = MetricsCalculator::new(Decimal::new(2, 2)); // Should handle empty data gracefully let returns = calculator.calculate_daily_returns().unwrap(); assert!(returns.is_empty()); let total_return = calculator.calculate_total_return().unwrap(); assert_eq!(total_return, Decimal::ZERO); } }