pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: f64) -> Option<f64> {
let (mean, std_dev) = mean_and_std(returns)?;
sharpe_with_stats(mean, std_dev, risk_free_rate, periods_per_year)
}
pub(crate) fn mean_and_std(returns: &[f64]) -> Option<(f64, f64)> {
if returns.len() < 2 {
return None;
}
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let variance =
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (returns.len() - 1) as f64;
Some((mean, variance.sqrt()))
}
pub(crate) fn sharpe_with_stats(
mean: f64,
std_dev: f64,
risk_free_rate: f64,
periods_per_year: f64,
) -> Option<f64> {
if std_dev == 0.0 {
return None;
}
Some((mean - risk_free_rate) / std_dev * periods_per_year.sqrt())
}
pub fn sortino_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: f64) -> Option<f64> {
if returns.len() < 2 {
return None;
}
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let downside_variance = returns
.iter()
.map(|r| {
let diff = r - risk_free_rate;
if diff < 0.0 { diff.powi(2) } else { 0.0 }
})
.sum::<f64>()
/ (returns.len() - 1) as f64;
let downside_std = downside_variance.sqrt();
if downside_std == 0.0 {
return None;
}
Some((mean - risk_free_rate) / downside_std * periods_per_year.sqrt())
}
pub fn omega_ratio(returns: &[f64]) -> f64 {
crate::perf_metrics::omega_ratio(returns)
}
pub fn kelly_criterion(win_rate: f64, avg_win_pct: f64, avg_loss_pct: f64) -> f64 {
crate::perf_metrics::kelly_criterion(win_rate, avg_win_pct, avg_loss_pct)
}
pub fn ulcer_index(returns: &[f64]) -> f64 {
crate::perf_metrics::ulcer_index(&super::drawdown::drawdown_series(returns))
}
pub fn information_ratio(
asset_returns: &[f64],
benchmark_returns: &[f64],
periods_per_year: f64,
) -> Option<f64> {
crate::perf_metrics::information_ratio(asset_returns, benchmark_returns, periods_per_year)
}
pub fn tracking_error(
asset_returns: &[f64],
benchmark_returns: &[f64],
periods_per_year: f64,
) -> Option<f64> {
crate::perf_metrics::tracking_error(asset_returns, benchmark_returns, periods_per_year)
}
pub fn win_loss_stats(returns: &[f64]) -> (f64, f64, f64) {
let total = returns.len();
if total == 0 {
return (0.0, 0.0, 0.0);
}
let wins: Vec<f64> = returns.iter().copied().filter(|&r| r > 0.0).collect();
let losses: Vec<f64> = returns.iter().copied().filter(|&r| r < 0.0).collect();
let win_rate = wins.len() as f64 / total as f64;
let avg_win_pct = if wins.is_empty() {
0.0
} else {
wins.iter().sum::<f64>() / wins.len() as f64 * 100.0
};
let avg_loss_pct = if losses.is_empty() {
0.0
} else {
losses.iter().sum::<f64>() / losses.len() as f64 * 100.0
};
(win_rate, avg_win_pct, avg_loss_pct)
}
pub fn calmar_ratio(total_return: f64, years: f64, max_drawdown: f64) -> Option<f64> {
if max_drawdown == 0.0 || years <= 0.0 {
return None;
}
let annualised = (1.0 + total_return).powf(1.0 / years) - 1.0;
Some(annualised / max_drawdown)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sharpe_positive_returns() {
let returns = vec![0.001_f64; 252];
let s = sharpe_ratio(&returns, 0.0, 252.0).unwrap();
assert!(s > 0.0, "Expected positive Sharpe, got {s}");
}
#[test]
fn test_sortino_only_positive() {
let returns = vec![0.01_f64; 252];
assert!(sortino_ratio(&returns, 0.0, 252.0).is_none());
}
#[test]
fn test_calmar_zero_drawdown() {
assert!(calmar_ratio(0.20, 2.0, 0.0).is_none());
}
#[test]
fn test_calmar_simple() {
let c = calmar_ratio(0.20, 2.0, 0.10).unwrap();
assert!((c - 0.954).abs() < 0.01, "got {c}");
}
}