Files
foxhunt/backtesting/src/metrics.rs
jgrusewski 72f607759a 🔧 Fix import paths: Remove non-existent prelude module references
- Fixed 101+ files importing common::types::prelude which doesn't exist
- Changed all imports to use common::types directly
- Fixed BarEvent duplicate import in data/src/types.rs
- Aligned all imports with canonical type system in common crate
2025-09-26 19:53:00 +02:00

1259 lines
38 KiB
Rust

//! 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, VecDeque},
sync::Arc,
};
use anyhow::{Context, Result};
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Serialize};
use statrs::statistics::{Statistics, VarianceN};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use common::types::*;
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<BenchmarkComparison>,
/// 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<Decimal>,
/// Monthly returns
pub monthly_returns: Vec<Decimal>,
/// 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<Decimal>,
/// Alpha (if benchmark provided)
pub alpha: Option<Decimal>,
/// Tracking error (if benchmark provided)
pub tracking_error: Option<Decimal>,
/// Information ratio (if benchmark provided)
pub information_ratio: Option<Decimal>,
}
/// 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<i64>,
/// Drawdown periods
pub drawdown_periods: Vec<DrawdownPeriod>,
/// Underwater curve
pub underwater_curve: Vec<(DateTime<Utc>, Decimal)>,
}
/// Individual drawdown period
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DrawdownPeriod {
/// Start date of drawdown
pub start_date: DateTime<Utc>,
/// End date of drawdown
pub end_date: Option<DateTime<Utc>>,
/// 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<DateTime<Utc>>,
}
/// 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<Symbol, u64>,
/// Monthly trade count
pub monthly_trade_count: Vec<(DateTime<Utc>, 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<Utc>,
/// Strategy end date
pub end_date: DateTime<Utc>,
/// Total days
pub total_days: i64,
/// Trading days
pub trading_days: i64,
/// Monthly performance
pub monthly_performance: Vec<MonthlyPerformance>,
/// Yearly performance
pub yearly_performance: Vec<YearlyPerformance>,
/// 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<Utc>,
/// 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<PerformanceSnapshot>,
/// Trade records
trades: Vec<TradeRecord>,
/// Benchmark data (if available)
benchmark_data: Option<Vec<(DateTime<Utc>, Decimal)>>,
/// Risk-free rate (annualized)
risk_free_rate: Decimal,
}
impl MetricsCalculator {
/// Create new metrics calculator
pub fn new(risk_free_rate: Decimal) -> Self {
Self {
snapshots: Vec::new(),
trades: Vec::new(),
benchmark_data: None,
risk_free_rate,
}
}
/// Add performance snapshot
pub fn add_snapshot(&mut self, snapshot: PerformanceSnapshot) {
self.snapshots.push(snapshot);
}
/// Add trade record
pub fn add_trade(&mut self, trade: TradeRecord) {
self.trades.push(trade);
}
/// Set benchmark data
pub fn set_benchmark(&mut self, benchmark_name: String, data: Vec<(DateTime<Utc>, Decimal)>) {
self.benchmark_data = Some(data);
}
/// Calculate comprehensive performance analytics
pub fn calculate_analytics(&self) -> Result<PerformanceAnalytics> {
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
fn calculate_return_metrics(&self) -> Result<ReturnMetrics> {
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<f64> = 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
fn calculate_risk_metrics(&self, returns: &ReturnMetrics) -> Result<RiskMetrics> {
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
fn calculate_drawdown_metrics(&self) -> Result<DrawdownMetrics> {
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
fn calculate_trade_statistics(&self) -> Result<TradeStatistics> {
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
fn calculate_benchmark_comparison(
&self,
returns: &ReturnMetrics,
) -> Result<Option<BenchmarkComparison>> {
if let Some(_benchmark_data) = &self.benchmark_data {
// Benchmark comparison implementation would go here
// Implementation for comprehensive benchmark analysis
warn!("Benchmark comparison not yet fully implemented");
Ok(None)
} else {
Ok(None)
}
}
/// Calculate portfolio metrics
fn calculate_portfolio_metrics(&self) -> Result<PortfolioMetrics> {
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::<u32>();
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
fn calculate_time_analysis(&self) -> Result<TimeAnalysis> {
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
fn calculate_daily_returns(&self) -> Result<Vec<Decimal>> {
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)
}
fn calculate_total_return(&self) -> Result<Decimal> {
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)
}
}
fn calculate_annualized_return(&self, daily_returns: &[Decimal]) -> Result<Decimal> {
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)
}
}
fn calculate_cagr(&self) -> Result<Decimal> {
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)
}
}
fn calculate_sharpe_ratio(&self, daily_returns: &[Decimal]) -> Result<Decimal> {
if daily_returns.is_empty() {
return Ok(Decimal::ZERO);
}
let returns_f64: Vec<f64> = 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::<f64>()
.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)
}
}
fn calculate_sortino_ratio(&self, daily_returns: &[Decimal]) -> Result<Decimal> {
if daily_returns.is_empty() {
return Ok(Decimal::ZERO);
}
let returns_f64: Vec<f64> = 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::<f64>()
.unwrap_or(0.0);
// Calculate downside deviation
let negative_returns: Vec<f64> = 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::<f64>() / 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)
}
}
fn calculate_calmar_ratio(&self, annualized_return: Decimal) -> Result<Decimal> {
let max_drawdown = self.calculate_max_drawdown()?;
if max_drawdown.abs() > Decimal::ZERO {
Ok(annualized_return / max_drawdown.abs())
} else {
Ok(Decimal::ZERO)
}
}
fn calculate_max_drawdown(&self) -> Result<Decimal> {
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)
}
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))
}
fn calculate_cvar(
&self,
daily_returns: &[Decimal],
confidence_level: Decimal,
) -> Result<Decimal> {
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::<f64>().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>() / Decimal::from(tail_returns.len());
Ok(cvar)
}
fn calculate_max_consecutive_losses(&self) -> Result<u32> {
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)
}
fn calculate_benchmark_risk_metrics(
&self,
_daily_returns: &[Decimal],
) -> Result<(
Option<Decimal>,
Option<Decimal>,
Option<Decimal>,
Option<Decimal>,
)> {
// Implementation for benchmark risk metrics calculation
Ok((None, None, None, None))
}
fn calculate_drawdowns(
&self,
) -> Result<(
Decimal,
Decimal,
Vec<DrawdownPeriod>,
Vec<(DateTime<Utc>, 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<DateTime<Utc>> = 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,
))
}
fn calculate_monthly_returns(&self) -> Result<Vec<Decimal>> {
// Implementation for monthly returns calculation
Ok(Vec::new())
}
fn calculate_monthly_trade_count(&self) -> Vec<(DateTime<Utc>, u64)> {
// Implementation for monthly trade count calculation
Vec::new()
}
fn calculate_monthly_performance(&self) -> Result<Vec<MonthlyPerformance>> {
// Implementation for monthly performance calculation
Ok(Vec::new())
}
fn calculate_yearly_performance(&self) -> Result<Vec<YearlyPerformance>> {
// Implementation for yearly performance calculation
Ok(Vec::new())
}
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::<f64>()
* n
/ ((n - 1.0) * (n - 2.0));
Decimal::from_f64_retain(skewness).unwrap_or_default()
}
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::<f64>()
* 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
trait DecimalPower {
fn powf(self, exp: f64) -> Decimal;
}
impl DecimalPower for Decimal {
fn powf(self, exp: f64) -> Decimal {
let base_f64 = self.to_string().parse::<f64>().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);
}
}