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 = "posei_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 use rstest::rstest;
78
79 use super::*;
80
81 fn create_returns(values: Vec<f64>) -> BTreeMap<UnixNanos, f64> {
82 let mut new_return = BTreeMap::new();
83 let one_day_in_nanos = 86_400_000_000_000;
84 let start_time = 1_600_000_000_000_000_000;
85
86 for (i, &value) in values.iter().enumerate() {
87 let timestamp = start_time + i as u64 * one_day_in_nanos;
88 new_return.insert(UnixNanos::from(timestamp), value);
89 }
90
91 new_return
92 }
93
94 #[rstest]
95 fn test_empty_returns() {
96 let ratio = SortinoRatio::new(None);
97 let returns = create_returns(vec![]);
98 let result = ratio.calculate_from_returns(&returns);
99 assert!(result.is_some());
100 assert!(result.unwrap().is_nan());
101 }
102
103 #[rstest]
104 fn test_zero_downside_deviation() {
105 let ratio = SortinoRatio::new(None);
106 let returns = create_returns(vec![0.02, 0.03, 0.01]);
107 let result = ratio.calculate_from_returns(&returns);
108 assert!(result.is_some());
109 assert!(result.unwrap().is_nan());
110 }
111
112 #[rstest]
113 fn test_valid_sortino_ratio() {
114 let ratio = SortinoRatio::new(Some(252));
115 let returns = create_returns(vec![-0.01, 0.02, -0.015, 0.005, -0.02]);
116 let result = ratio.calculate_from_returns(&returns);
117 assert!(result.is_some());
118 assert_eq!(result.unwrap(), -5.273224492824493);
119 }
120
121 #[rstest]
122 fn test_name() {
123 let ratio = SortinoRatio::new(None);
124 assert_eq!(ratio.name(), "SortinoRatio");
125 }
126}