#[inline]
fn safe_ratio(numerator: f64, denominator: f64, fallback: f64) -> f64 {
const EPSILON: f64 = 1e-10;
if denominator > EPSILON {
numerator / denominator
} else if numerator > 0.0 {
f64::INFINITY
} else if numerator < 0.0 {
f64::NEG_INFINITY
} else {
fallback
}
}
fn mean_and_std(returns: &[f64]) -> Option<(f64, f64)> {
if returns.len() < 2 {
return None;
}
let n = returns.len() as f64;
let mean = returns.iter().sum::<f64>() / n;
let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
Some((mean, variance.sqrt()))
}
#[must_use]
pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64) -> f64 {
let Some((mean, std)) = mean_and_std(returns) else {
return 0.0;
};
safe_ratio(mean - risk_free_rate, std, 0.0)
}
#[must_use]
pub fn sharpe_ratio_annualized(returns: &[f64], risk_free_rate: f64, periods_per_year: f64) -> f64 {
let Some((mean, std)) = mean_and_std(returns) else {
return 0.0;
};
let annualized_return = mean * periods_per_year;
let annualized_vol = std * periods_per_year.sqrt();
safe_ratio(annualized_return - risk_free_rate, annualized_vol, 0.0)
}
#[must_use]
pub fn sortino_ratio(returns: &[f64], risk_free_rate: f64, target_return: f64) -> f64 {
if returns.len() < 2 {
return 0.0;
}
let n = returns.len() as f64;
let mean = returns.iter().sum::<f64>() / n;
let excess_return = mean - risk_free_rate;
let downside_sq_sum: f64 = returns
.iter()
.filter(|&&r| r < target_return)
.map(|&r| (r - target_return).powi(2))
.sum();
let downside_deviation = (downside_sq_sum / n).sqrt();
safe_ratio(excess_return, downside_deviation, 0.0)
}
#[must_use]
pub fn calmar_ratio(annualized_return: f64, max_drawdown: f64) -> f64 {
safe_ratio(annualized_return, max_drawdown, 0.0)
}
#[must_use]
pub fn treynor_ratio(returns: &[f64], benchmark_returns: &[f64], risk_free_rate: f64) -> f64 {
let beta = calculate_beta(returns, benchmark_returns);
if returns.is_empty() {
return 0.0;
}
let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
let excess_return = mean_return - risk_free_rate;
if beta.abs() > 1e-10 {
excess_return / beta
} else {
0.0
}
}
#[must_use]
pub fn information_ratio(returns: &[f64], benchmark_returns: &[f64]) -> f64 {
if returns.len() != benchmark_returns.len() || returns.len() < 2 {
return 0.0;
}
let n = returns.len() as f64;
let active_returns: Vec<f64> = returns
.iter()
.zip(benchmark_returns.iter())
.map(|(r, b)| r - b)
.collect();
let mean_active = active_returns.iter().sum::<f64>() / n;
let tracking_variance = active_returns
.iter()
.map(|r| (r - mean_active).powi(2))
.sum::<f64>()
/ (n - 1.0);
let tracking_error = tracking_variance.sqrt();
if tracking_error > 1e-10 {
mean_active / tracking_error
} else {
0.0
}
}
#[must_use]
pub fn omega_ratio(returns: &[f64], threshold: f64) -> f64 {
if returns.is_empty() {
return 1.0;
}
let gains: f64 = returns
.iter()
.filter(|&&r| r > threshold)
.map(|&r| r - threshold)
.sum();
let losses: f64 = returns
.iter()
.filter(|&&r| r < threshold)
.map(|&r| threshold - r)
.sum();
safe_ratio(gains, losses, 1.0)
}
fn calculate_beta(returns: &[f64], benchmark_returns: &[f64]) -> f64 {
if returns.len() != benchmark_returns.len() || returns.len() < 2 {
return 1.0;
}
let n = returns.len() as f64;
let mean_r = returns.iter().sum::<f64>() / n;
let mean_b = benchmark_returns.iter().sum::<f64>() / n;
let covariance: f64 = returns
.iter()
.zip(benchmark_returns.iter())
.map(|(r, b)| (r - mean_r) * (b - mean_b))
.sum::<f64>()
/ (n - 1.0);
let var_benchmark: f64 = benchmark_returns
.iter()
.map(|b| (b - mean_b).powi(2))
.sum::<f64>()
/ (n - 1.0);
if var_benchmark > 1e-10 {
covariance / var_benchmark
} else {
1.0
}
}
#[must_use]
pub fn jensens_alpha(returns: &[f64], benchmark_returns: &[f64], risk_free_rate: f64) -> f64 {
if returns.is_empty() || benchmark_returns.is_empty() {
return 0.0;
}
let mean_r = returns.iter().sum::<f64>() / returns.len() as f64;
let mean_b = benchmark_returns.iter().sum::<f64>() / benchmark_returns.len() as f64;
let beta = calculate_beta(returns, benchmark_returns);
mean_r - (risk_free_rate + beta * (mean_b - risk_free_rate))
}
#[must_use]
pub fn gain_to_pain_ratio(returns: &[f64]) -> f64 {
if returns.is_empty() {
return 0.0;
}
let gains: f64 = returns.iter().filter(|&&r| r > 0.0).sum();
let losses: f64 = returns.iter().filter(|&&r| r < 0.0).map(|r| r.abs()).sum();
safe_ratio(gains, losses, 0.0)
}
#[path = "sample.rs"]
mod sample;