#[cfg(test)]
mod tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
fn sample_returns() -> Vec<f64> {
vec![
0.01, 0.02, -0.01, 0.03, 0.01, -0.02, 0.02, 0.01, 0.005, -0.015,
]
}
fn sample_benchmark() -> Vec<f64> {
vec![
0.008, 0.015, -0.005, 0.025, 0.01, -0.01, 0.018, 0.008, 0.003, -0.012,
]
}
#[test]
fn test_sharpe_ratio_positive() {
let returns = sample_returns();
let sharpe = sharpe_ratio(&returns, 0.001);
assert!(sharpe > 0.0, "Sharpe should be positive: {sharpe}");
assert!(sharpe.is_finite());
}
#[test]
fn test_sharpe_ratio_zero_variance() {
let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01];
let sharpe = sharpe_ratio(&returns, 0.001);
assert!(
sharpe.is_infinite() && sharpe > 0.0,
"Sharpe with zero variance: {sharpe}"
);
}
#[test]
fn test_sharpe_ratio_annualized() {
let returns = sample_returns();
let sharpe_daily = sharpe_ratio_annualized(&returns, 0.05, 252.0);
let sharpe_monthly = sharpe_ratio_annualized(&returns, 0.05, 12.0);
assert!(sharpe_daily.is_finite());
assert!(sharpe_monthly.is_finite());
}
#[test]
fn test_sortino_ratio() {
let returns = sample_returns();
let sortino = sortino_ratio(&returns, 0.001, 0.0);
assert!(sortino > 0.0, "Sortino should be positive: {sortino}");
assert!(sortino.is_finite());
}
#[test]
fn test_sortino_no_downside() {
let returns = vec![0.01, 0.02, 0.03, 0.04, 0.05];
let sortino = sortino_ratio(&returns, 0.0, 0.0);
assert!(
sortino.is_infinite() && sortino > 0.0,
"Sortino with no downside: {sortino}"
);
}
#[test]
fn test_calmar_ratio() {
let calmar = calmar_ratio(0.15, 0.10);
assert!((calmar - 1.5).abs() < 0.001);
let calmar_high = calmar_ratio(0.30, 0.10);
assert!((calmar_high - 3.0).abs() < 0.001);
}
#[test]
fn test_calmar_ratio_zero_drawdown() {
let calmar = calmar_ratio(0.10, 0.0);
assert!(calmar.is_infinite() && calmar > 0.0);
}
#[test]
fn test_treynor_ratio() {
let returns = sample_returns();
let benchmark = sample_benchmark();
let treynor = treynor_ratio(&returns, &benchmark, 0.001);
assert!(treynor.is_finite());
}
#[test]
fn test_information_ratio() {
let returns = sample_returns();
let benchmark = sample_benchmark();
let ir = information_ratio(&returns, &benchmark);
assert!(ir.is_finite());
}
#[test]
fn test_omega_ratio() {
let returns = sample_returns();
let omega = omega_ratio(&returns, 0.0);
assert!(omega > 0.0, "Omega should be positive: {omega}");
}
#[test]
fn test_omega_ratio_threshold() {
let returns = sample_returns();
let omega_0 = omega_ratio(&returns, 0.0);
let omega_high = omega_ratio(&returns, 0.02);
assert!(omega_high < omega_0, "Higher threshold = lower omega");
}
#[test]
fn test_jensens_alpha() {
let returns = sample_returns();
let benchmark = sample_benchmark();
let alpha = jensens_alpha(&returns, &benchmark, 0.001);
assert!(alpha.is_finite());
}
#[test]
fn test_gain_to_pain_ratio() {
let returns = sample_returns();
let gpr = gain_to_pain_ratio(&returns);
assert!(gpr > 0.0);
assert!(gpr.is_finite());
}
#[test]
fn test_gain_to_pain_all_positive() {
let returns = vec![0.01, 0.02, 0.03, 0.04];
let gpr = gain_to_pain_ratio(&returns);
assert!(gpr.is_infinite() && gpr > 0.0);
}
#[test]
fn test_calculate_beta() {
let returns = sample_returns();
let benchmark = sample_benchmark();
let beta = calculate_beta(&returns, &benchmark);
assert!(
beta > 0.0 && beta < 3.0,
"Beta should be reasonable: {beta}"
);
}
#[test]
fn test_empty_inputs() {
assert!(sharpe_ratio(&[], 0.0).abs() < 1e-10);
assert!(sortino_ratio(&[], 0.0, 0.0).abs() < 1e-10);
assert!(information_ratio(&[], &[]).abs() < 1e-10);
assert!((omega_ratio(&[], 0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_single_value() {
let returns = vec![0.01];
assert!(sharpe_ratio(&returns, 0.0).abs() < 1e-10);
assert!(sortino_ratio(&returns, 0.0, 0.0).abs() < 1e-10);
}
#[test]
fn test_sharpe_ratio_zero_variance_negative_excess() {
let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01];
let sharpe = sharpe_ratio(&returns, 0.05);
assert!(
sharpe.is_infinite() && sharpe < 0.0,
"Sharpe with zero variance and negative excess should be -Infinity: {sharpe}"
);
}
#[test]
fn test_sharpe_ratio_zero_variance_zero_excess() {
let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01];
let sharpe = sharpe_ratio(&returns, 0.01);
assert!(
sharpe.abs() < 1e-10,
"Sharpe with zero variance and zero excess should be 0: {sharpe}"
);
}
#[test]
fn test_sharpe_annualized_short_returns() {
let sharpe = sharpe_ratio_annualized(&[0.01], 0.05, 252.0);
assert!(sharpe.abs() < 1e-10);
}
#[test]
fn test_sharpe_annualized_zero_vol_positive_excess() {
let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01];
let sharpe = sharpe_ratio_annualized(&returns, 0.0, 252.0);
assert!(
sharpe.is_infinite() && sharpe > 0.0,
"Annualized Sharpe with zero vol and positive excess should be +Infinity: {sharpe}"
);
}
#[test]
fn test_sharpe_annualized_zero_vol_negative_excess() {
let returns = vec![0.001, 0.001, 0.001, 0.001, 0.001];
let sharpe = sharpe_ratio_annualized(&returns, 10.0, 252.0);
assert!(
sharpe.is_infinite() && sharpe < 0.0,
"Annualized Sharpe with zero vol and negative excess should be -Infinity: {sharpe}"
);
}
#[test]
fn test_sharpe_annualized_zero_vol_zero_excess() {
let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01];
let sharpe = sharpe_ratio_annualized(&returns, 0.12, 12.0);
assert!(
sharpe.abs() < 1e-10,
"Annualized Sharpe with zero vol and zero excess should be 0: {sharpe}"
);
}
#[test]
fn test_sortino_zero_downside_negative_excess() {
let returns = vec![0.01, 0.02, 0.03, 0.04, 0.05];
let sortino = sortino_ratio(&returns, 0.10, 0.0);
assert!(
sortino.is_infinite() && sortino < 0.0,
"Sortino with zero downside and negative excess should be -Infinity: {sortino}"
);
}
#[test]
fn test_sortino_zero_downside_zero_excess() {
let returns = vec![0.03, 0.03, 0.03, 0.03, 0.03];
let sortino = sortino_ratio(&returns, 0.03, 0.0);
assert!(
sortino.abs() < 1e-10,
"Sortino with zero downside and zero excess should be 0: {sortino}"
);
}
#[test]
fn test_calmar_zero_drawdown_negative_return() {
let calmar = calmar_ratio(-0.10, 0.0);
assert!(
calmar.is_infinite() && calmar < 0.0,
"Calmar with zero drawdown and negative return should be -Infinity: {calmar}"
);
}
#[test]
fn test_calmar_zero_drawdown_zero_return() {
let calmar = calmar_ratio(0.0, 0.0);
assert!(
calmar.abs() < 1e-10,
"Calmar with zero drawdown and zero return should be 0: {calmar}"
);
}
#[test]
fn test_treynor_empty_returns() {
let treynor = treynor_ratio(&[], &[], 0.01);
assert!(
treynor.abs() < 1e-10,
"Treynor with empty returns should be 0: {treynor}"
);
}
#[test]
fn test_treynor_mismatched_lengths() {
let returns = vec![0.01, 0.02, 0.03];
let benchmark = vec![0.01, 0.02];
let treynor = treynor_ratio(&returns, &benchmark, 0.0);
assert!(treynor.is_finite());
}
#[test]
fn test_treynor_zero_beta() {
let returns = vec![0.01, 0.02, 0.03, 0.04];
let benchmark = vec![0.05, 0.05, 0.05, 0.05]; let treynor = treynor_ratio(&returns, &benchmark, 0.0);
assert!(treynor.is_finite());
assert!(treynor > 0.0);
}
#[test]
fn test_information_ratio_mismatched_lengths() {
let returns = vec![0.01, 0.02, 0.03];
let benchmark = vec![0.01, 0.02];
let ir = information_ratio(&returns, &benchmark);
assert!(
ir.abs() < 1e-10,
"IR with mismatched lengths should be 0: {ir}"
);
}
#[test]
fn test_information_ratio_single_value() {
let ir = information_ratio(&[0.01], &[0.01]);
assert!(ir.abs() < 1e-10, "IR with single value should be 0: {ir}");
}
#[test]
fn test_omega_ratio_all_above_threshold() {
let returns = vec![0.05, 0.06, 0.07, 0.08];
let omega = omega_ratio(&returns, 0.0);
assert!(
omega.is_infinite() && omega > 0.0,
"Omega with no losses and gains should be Infinity: {omega}"
);
}
#[test]
fn test_omega_ratio_all_equal_threshold() {
let returns = vec![0.0, 0.0, 0.0, 0.0];
let omega = omega_ratio(&returns, 0.0);
assert!(
(omega - 1.0).abs() < 1e-10,
"Omega with all returns at threshold should be 1.0: {omega}"
);
}
#[test]
fn test_gain_to_pain_all_negative() {
let returns = vec![-0.01, -0.02, -0.03, -0.04];
let gpr = gain_to_pain_ratio(&returns);
assert!(
gpr.abs() < 1e-10,
"Gain-to-pain with all negative and no gains should be 0: {gpr}"
);
}
#[test]
fn test_gain_to_pain_all_zero() {
let returns = vec![0.0, 0.0, 0.0];
let gpr = gain_to_pain_ratio(&returns);
assert!(
gpr.abs() < 1e-10,
"Gain-to-pain with all zeros should be 0: {gpr}"
);
}
#[test]
fn test_gain_to_pain_empty() {
let gpr = gain_to_pain_ratio(&[]);
assert!(
gpr.abs() < 1e-10,
"Gain-to-pain with empty should be 0: {gpr}"
);
}
#[test]
fn test_jensens_alpha_empty_returns() {
let alpha = jensens_alpha(&[], &[0.01, 0.02], 0.01);
assert!(
alpha.abs() < 1e-10,
"Alpha with empty returns should be 0: {alpha}"
);
}
#[test]
fn test_jensens_alpha_empty_benchmark() {
let alpha = jensens_alpha(&[0.01, 0.02], &[], 0.01);
assert!(
alpha.abs() < 1e-10,
"Alpha with empty benchmark should be 0: {alpha}"
);
}
#[test]
fn test_calculate_beta_mismatched() {
let beta = calculate_beta(&[0.01, 0.02, 0.03], &[0.01, 0.02]);
assert!(
(beta - 1.0).abs() < 1e-10,
"Beta with mismatched lengths should be 1.0: {beta}"
);
}
#[test]
fn test_calculate_beta_single_value() {
let beta = calculate_beta(&[0.01], &[0.02]);
assert!(
(beta - 1.0).abs() < 1e-10,
"Beta with single value should be 1.0: {beta}"
);
}
#[test]
fn test_calculate_beta_zero_benchmark_variance() {
let beta = calculate_beta(&[0.01, 0.02, 0.03], &[0.05, 0.05, 0.05]);
assert!(
(beta - 1.0).abs() < 1e-10,
"Beta with zero benchmark variance should be 1.0: {beta}"
);
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_sharpe_finite(returns in prop::collection::vec(-0.5..0.5f64, 10..100)) {
let sharpe = sharpe_ratio(&returns, 0.01);
prop_assert!(sharpe.is_finite() || sharpe.is_infinite(), "Sharpe must be defined");
}
#[test]
fn prop_sortino_geq_zero_or_negative(
returns in prop::collection::vec(-0.5..0.5f64, 10..100)
) {
let sortino = sortino_ratio(&returns, 0.0, 0.0);
prop_assert!(sortino.is_finite() || sortino.is_infinite());
}
#[test]
fn prop_calmar_sign_matches_return(
ret in -0.5..0.5f64,
dd in 0.01..0.5f64,
) {
let calmar = calmar_ratio(ret, dd);
if ret > 0.0 {
prop_assert!(calmar > 0.0);
} else if ret < 0.0 {
prop_assert!(calmar < 0.0);
}
}
#[test]
fn prop_omega_positive(
returns in prop::collection::vec(-0.1..0.1f64, 10..100),
threshold in -0.1..0.1f64,
) {
let omega = omega_ratio(&returns, threshold);
prop_assert!(omega >= 0.0 || omega.is_infinite(), "Omega must be non-negative");
}
#[test]
fn prop_information_ratio_finite(
returns in prop::collection::vec(-0.1..0.1f64, 20..100),
benchmark in prop::collection::vec(-0.1..0.1f64, 20..100),
) {
let len = returns.len().min(benchmark.len());
let ir = information_ratio(&returns[..len], &benchmark[..len]);
prop_assert!(ir.is_finite() || ir.abs() < 1e-10, "IR should be finite: {ir}");
}
}
}
}