nautilus_analysis/statistics/
sortino_ratio.rs1use crate::{Returns, statistic::PortfolioStatistic};
17
18#[repr(C)]
19#[derive(Debug)]
20#[cfg_attr(
21 feature = "python",
22 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
23)]
24pub struct SortinoRatio {
25 period: usize,
26}
27
28impl SortinoRatio {
29 #[must_use]
31 pub fn new(period: Option<usize>) -> Self {
32 Self {
33 period: period.unwrap_or(252),
34 }
35 }
36}
37
38impl PortfolioStatistic for SortinoRatio {
39 type Item = f64;
40
41 fn name(&self) -> String {
42 stringify!(SortinoRatio).to_string()
43 }
44
45 fn calculate_from_returns(&self, raw_returns: &Returns) -> Option<Self::Item> {
46 if !self.check_valid_returns(raw_returns) {
47 return Some(f64::NAN);
48 }
49
50 let returns = self.downsample_to_daily_bins(raw_returns);
51 let total_n = returns.len() as f64;
52 let mean = returns.values().sum::<f64>() / total_n;
53
54 let downside = (returns
55 .values()
56 .filter(|&&x| x < 0.0)
57 .map(|x| x.powi(2))
58 .sum::<f64>()
59 / total_n)
60 .sqrt();
61
62 if downside < f64::EPSILON {
63 return Some(f64::NAN);
64 }
65
66 let annualized_ratio = (mean / downside) * (self.period as f64).sqrt();
67
68 Some(annualized_ratio)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use std::collections::BTreeMap;
75
76 use nautilus_core::UnixNanos;
77
78 use super::*;
79
80 fn create_returns(values: Vec<f64>) -> BTreeMap<UnixNanos, f64> {
81 let mut new_return = BTreeMap::new();
82 let one_day_in_nanos = 86_400_000_000_000;
83 let start_time = 1_600_000_000_000_000_000;
84
85 for (i, &value) in values.iter().enumerate() {
86 let timestamp = start_time + i as u64 * one_day_in_nanos;
87 new_return.insert(UnixNanos::from(timestamp), value);
88 }
89
90 new_return
91 }
92
93 #[test]
94 fn test_empty_returns() {
95 let ratio = SortinoRatio::new(None);
96 let returns = create_returns(vec![]);
97 let result = ratio.calculate_from_returns(&returns);
98 assert!(result.is_some());
99 assert!(result.unwrap().is_nan());
100 }
101
102 #[test]
103 fn test_zero_downside_deviation() {
104 let ratio = SortinoRatio::new(None);
105 let returns = create_returns(vec![0.02, 0.03, 0.01]);
106 let result = ratio.calculate_from_returns(&returns);
107 assert!(result.is_some());
108 assert!(result.unwrap().is_nan());
109 }
110
111 #[test]
112 fn test_valid_sortino_ratio() {
113 let ratio = SortinoRatio::new(Some(252));
114 let returns = create_returns(vec![-0.01, 0.02, -0.015, 0.005, -0.02]);
115 let result = ratio.calculate_from_returns(&returns);
116 assert!(result.is_some());
117 assert_eq!(result.unwrap(), -5.273224492824493);
118 }
119
120 #[test]
121 fn test_name() {
122 let ratio = SortinoRatio::new(None);
123 assert_eq!(ratio.name(), "SortinoRatio");
124 }
125}