use crate::statistic::time::TimeInterval;
use rust_decimal::{Decimal, MathematicalOps};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize)]
pub struct SortinoRatio<Interval> {
pub value: Decimal,
pub interval: Interval,
}
impl<Interval> SortinoRatio<Interval>
where
Interval: TimeInterval,
{
pub fn calculate(
risk_free_return: Decimal,
mean_return: Decimal,
std_dev_loss_returns: Decimal,
returns_period: Interval,
) -> Self {
if std_dev_loss_returns.is_zero() {
Self {
value: match mean_return.cmp(&risk_free_return) {
Ordering::Greater => Decimal::MAX,
Ordering::Less => Decimal::MIN,
Ordering::Equal => Decimal::ZERO,
},
interval: returns_period,
}
} else {
let excess_returns = mean_return - risk_free_return;
let ratio = excess_returns.checked_div(std_dev_loss_returns).unwrap();
Self {
value: ratio,
interval: returns_period,
}
}
}
pub fn scale<TargetInterval>(self, target: TargetInterval) -> SortinoRatio<TargetInterval>
where
TargetInterval: TimeInterval,
{
let target_secs = Decimal::from(target.interval().num_seconds());
let current_secs = Decimal::from(self.interval.interval().num_seconds());
let scale = target_secs
.abs()
.checked_div(current_secs.abs())
.unwrap_or(Decimal::MAX)
.sqrt()
.expect("ensured seconds are Positive");
SortinoRatio {
value: self.value.checked_mul(scale).unwrap_or(Decimal::MAX),
interval: target,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::statistic::time::{Annual252, Daily};
use chrono::TimeDelta;
use rust_decimal_macros::dec;
use std::str::FromStr;
#[test]
fn test_sortino_ratio_normal_case() {
let risk_free_return = dec!(0.0015); let mean_return = dec!(0.0025); let std_dev_loss_returns = dec!(0.02); let time_period = Daily;
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
let expected = SortinoRatio {
value: dec!(0.05), interval: time_period,
};
assert_eq!(actual.value, expected.value);
assert_eq!(actual.interval, time_period);
}
#[test]
fn test_sortino_ratio_zero_downside_dev_positive_excess() {
let risk_free_return = dec!(0.001); let mean_return = dec!(0.002); let std_dev_loss_returns = dec!(0.0);
let time_period = Daily;
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
assert_eq!(actual.value, Decimal::MAX);
assert_eq!(actual.interval, time_period);
}
#[test]
fn test_sortino_ratio_zero_downside_dev_negative_excess() {
let risk_free_return = dec!(0.002); let mean_return = dec!(0.001); let std_dev_loss_returns = dec!(0.0);
let time_period = Daily;
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
assert_eq!(actual.value, Decimal::MIN);
assert_eq!(actual.interval, time_period);
}
#[test]
fn test_sortino_ratio_zero_downside_dev_no_excess() {
let risk_free_return = dec!(0.001); let mean_return = dec!(0.001); let std_dev_loss_returns = dec!(0.0);
let time_period = Daily;
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
assert_eq!(actual.value, dec!(0.0));
assert_eq!(actual.interval, time_period);
}
#[test]
fn test_sortino_ratio_negative_returns() {
let risk_free_return = dec!(0.001); let mean_return = dec!(-0.002); let std_dev_loss_returns = dec!(0.015); let time_period = Daily;
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
let expected = SortinoRatio {
value: dec!(-0.2), interval: time_period,
};
assert_eq!(actual.value, expected.value);
assert_eq!(actual.interval, expected.interval);
}
#[test]
fn test_sortino_ratio_custom_interval() {
let risk_free_return = dec!(0.0015); let mean_return = dec!(0.0025); let std_dev_loss_returns = dec!(0.02); let time_period = TimeDelta::hours(4);
let actual = SortinoRatio::calculate(
risk_free_return,
mean_return,
std_dev_loss_returns,
time_period,
);
let expected = SortinoRatio {
value: dec!(0.05),
interval: time_period,
};
assert_eq!(actual.value, expected.value);
assert_eq!(actual.interval, expected.interval);
}
#[test]
fn test_sortino_ratio_scale_daily_to_annual() {
let daily = SortinoRatio {
value: dec!(0.05),
interval: Daily,
};
let actual = daily.scale(Annual252);
let expected = SortinoRatio {
value: Decimal::from_str("0.7937").unwrap(),
interval: Annual252,
};
let diff = (actual.value - expected.value).abs();
assert!(diff <= Decimal::from_str("0.0001").unwrap());
assert_eq!(actual.interval, expected.interval);
}
#[test]
fn test_sortino_ratio_scale_custom_intervals() {
let two_hour = SortinoRatio {
value: dec!(0.05),
interval: TimeDelta::hours(2),
};
let actual = two_hour.scale(TimeDelta::hours(8));
let expected = SortinoRatio {
value: dec!(0.1),
interval: TimeDelta::hours(8),
};
assert_eq!(actual.value, expected.value);
assert_eq!(actual.interval, expected.interval);
}
#[test]
fn test_sortino_ratio_extreme_values() {
let small = SortinoRatio::calculate(
Decimal::from_scientific("1e-10").unwrap(),
Decimal::from_scientific("2e-10").unwrap(),
Decimal::from_scientific("1e-10").unwrap(),
Daily,
);
let diff = (small.value - dec!(1.0)).abs();
assert!(diff <= Decimal::from_str("0.0001").unwrap());
let large = SortinoRatio::calculate(
Decimal::from_scientific("1e10").unwrap(),
Decimal::from_scientific("2e10").unwrap(),
Decimal::from_scientific("1e10").unwrap(),
Daily,
);
let diff = (large.value - dec!(1.0)).abs();
assert!(diff <= Decimal::from_str("0.0001").unwrap());
}
}