use crate::errors::QlResult;
use crate::{ensure, require};
fn validate(returns: &[f64], target: f64, minimum: usize) -> QlResult<()> {
require!(returns.len() >= minimum, "insufficient return observations");
require!(target.is_finite(), "per-period target must be finite");
for &value in returns {
require!(value.is_finite(), "return observations must be finite");
let excess = value - target;
ensure!(excess.is_finite(), "nonfinite excess return");
}
Ok(())
}
fn downside_scale(returns: &[f64], target: f64) -> f64 {
returns
.iter()
.map(|&value| (target - value).max(0.0))
.fold(0.0, f64::max)
}
fn downside_scaled(returns: &[f64], target: f64, scale: f64) -> f64 {
if scale == 0.0 {
return 0.0;
}
let sum: f64 = returns
.iter()
.map(|&value| (((target - value).max(0.0)) / scale).powi(2))
.sum();
(sum / returns.len() as f64).sqrt()
}
fn moments(returns: &[f64]) -> (f64, f64, f64, f64) {
let origin = returns[0];
let anchored = returns.iter().all(|value| (value - origin).is_finite());
let shift = if anchored { origin } else { 0.0 };
let scale = returns
.iter()
.map(|value| (value - shift).abs())
.fold(0.0, f64::max);
if scale == 0.0 {
return (origin, 0.0, 0.0, 0.0);
}
let mut sum = 0.0;
let mut correction = 0.0;
for &value in returns {
let term = (value - shift) / scale - correction;
let next = sum + term;
correction = (next - sum) - term;
sum = next;
}
let center = (sum / returns.len() as f64).clamp(-1.0, 1.0);
let squares = returns
.iter()
.map(|value| ((value - shift) / scale - center).powi(2))
.sum::<f64>();
let deviation = if returns.len() > 1 {
(squares / (returns.len() - 1) as f64).sqrt()
} else {
0.0
};
(shift, scale, center, deviation)
}
pub fn target_downside_deviation(returns: &[f64], target: f64) -> QlResult<f64> {
validate(returns, target, 1)?;
let scale = downside_scale(returns, target);
let result = scale * downside_scaled(returns, target, scale);
ensure!(result.is_finite(), "nonfinite target downside deviation");
Ok(result)
}
fn ratio_inputs(returns: &[f64], target: f64, periods_per_year: f64) -> QlResult<(f64, f64, f64)> {
require!(
periods_per_year.is_finite() && periods_per_year > 0.0,
"periods per year must be finite and positive"
);
validate(returns, target, 2)?;
let (shift, scale, center, deviation) = moments(returns);
let excess_mean = (shift - target) + scale * center;
ensure!(excess_mean.is_finite(), "nonfinite arithmetic excess mean");
Ok((excess_mean, scale, deviation))
}
fn annualize(mean_scaled: f64, deviation_scaled: f64, periods_per_year: f64) -> QlResult<f64> {
require!(
deviation_scaled.is_finite() && deviation_scaled > 0.0,
"performance ratio denominator is zero"
);
let result = (mean_scaled / deviation_scaled) * periods_per_year.sqrt();
ensure!(result.is_finite(), "nonfinite performance ratio");
Ok(result)
}
pub fn sharpe_ratio(
returns: &[f64],
risk_free_return: f64,
periods_per_year: f64,
) -> QlResult<f64> {
let (excess_mean, scale, deviation) =
ratio_inputs(returns, risk_free_return, periods_per_year)?;
require!(scale != 0.0, "performance ratio denominator is zero");
annualize(excess_mean / scale, deviation, periods_per_year)
}
pub fn sortino_ratio(
returns: &[f64],
minimum_acceptable_return: f64,
periods_per_year: f64,
) -> QlResult<f64> {
let (excess_mean, _, _) = ratio_inputs(returns, minimum_acceptable_return, periods_per_year)?;
let scale = downside_scale(returns, minimum_acceptable_return);
require!(scale != 0.0, "performance ratio denominator is zero");
annualize(
excess_mean / scale,
downside_scaled(returns, minimum_acceptable_return, scale),
periods_per_year,
)
}