use rust_decimal::Decimal;
use crate::drawdown::max_drawdown;
use crate::returns::annualized_return;
use crate::MetricsError;
pub fn sharpe_ratio(
equity: &[Decimal],
risk_free_rate: Decimal,
periods_per_year: u32,
) -> Result<Decimal, MetricsError> {
if equity.len() < 3 {
return Err(MetricsError::InsufficientData {
required: 3,
actual: equity.len(),
});
}
let returns = period_returns(equity);
let mean_return = mean(&returns);
let std_dev = std_deviation(&returns);
if std_dev == Decimal::ZERO {
return Err(MetricsError::DivisionByZero {
context: "zero volatility",
});
}
let period_rf = risk_free_rate / Decimal::from(periods_per_year);
let excess_return = mean_return - period_rf;
let sqrt_periods = decimal_sqrt(Decimal::from(periods_per_year));
Ok((excess_return / std_dev) * sqrt_periods)
}
pub fn sortino_ratio(
equity: &[Decimal],
risk_free_rate: Decimal,
periods_per_year: u32,
) -> Result<Decimal, MetricsError> {
if equity.len() < 3 {
return Err(MetricsError::InsufficientData {
required: 3,
actual: equity.len(),
});
}
let returns = period_returns(equity);
let mean_return = mean(&returns);
let downside_dev = downside_deviation(&returns, Decimal::ZERO);
if downside_dev == Decimal::ZERO {
return Err(MetricsError::DivisionByZero {
context: "zero downside deviation",
});
}
let period_rf = risk_free_rate / Decimal::from(periods_per_year);
let excess_return = mean_return - period_rf;
let sqrt_periods = decimal_sqrt(Decimal::from(periods_per_year));
Ok((excess_return / downside_dev) * sqrt_periods)
}
pub fn calmar_ratio(equity: &[Decimal], periods_per_year: u32) -> Result<Decimal, MetricsError> {
let ann_return = annualized_return(equity, periods_per_year)?;
let max_dd = max_drawdown(equity)?;
if max_dd == Decimal::ZERO {
return Err(MetricsError::DivisionByZero {
context: "zero max drawdown",
});
}
Ok(ann_return / max_dd.abs())
}
fn period_returns(equity: &[Decimal]) -> Vec<Decimal> {
equity
.windows(2)
.filter_map(|w| {
if w[0] == Decimal::ZERO {
None
} else {
Some((w[1] - w[0]) / w[0])
}
})
.collect()
}
use crate::math::{decimal_sqrt, mean, std_deviation};
fn downside_deviation(returns: &[Decimal], threshold: Decimal) -> Decimal {
let downside: Vec<Decimal> = returns
.iter()
.filter(|&&r| r < threshold)
.map(|&r| (r - threshold) * (r - threshold))
.collect();
if downside.is_empty() {
return Decimal::ZERO;
}
let sum: Decimal = downside.iter().sum();
let variance = sum / Decimal::from(returns.len() as u64);
decimal_sqrt(variance)
}
pub fn information_ratio(
portfolio: &[Decimal],
benchmark: &[Decimal],
periods_per_year: u32,
) -> Result<Decimal, MetricsError> {
if portfolio.len() != benchmark.len() {
return Err(MetricsError::InvalidParameter(
"portfolio and benchmark must have same length".into(),
));
}
if portfolio.len() < 3 {
return Err(MetricsError::InsufficientData {
required: 3,
actual: portfolio.len(),
});
}
let portfolio_returns = period_returns(portfolio);
let benchmark_returns = period_returns(benchmark);
let active_returns: Vec<Decimal> = portfolio_returns
.iter()
.zip(benchmark_returns.iter())
.map(|(&p, &b)| p - b)
.collect();
let mean_active = mean(&active_returns);
let tracking_error = std_deviation(&active_returns);
if tracking_error == Decimal::ZERO {
return Err(MetricsError::DivisionByZero {
context: "zero tracking error",
});
}
let sqrt_periods = decimal_sqrt(Decimal::from(periods_per_year));
Ok((mean_active / tracking_error) * sqrt_periods)
}
#[cfg(test)]
#[path = "risk_adjusted_tests.rs"]
mod tests;