nautilus_analysis/statistics/
sharpe_ratio.rs1use crate::{Returns, statistic::PortfolioStatistic};
17
18#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.analysis")
30)]
31pub struct SharpeRatio {
32 period: usize,
34}
35
36impl SharpeRatio {
37 #[must_use]
39 pub fn new(period: Option<usize>) -> Self {
40 Self {
41 period: period.unwrap_or(252),
42 }
43 }
44}
45
46impl PortfolioStatistic for SharpeRatio {
47 type Item = f64;
48
49 fn name(&self) -> String {
50 stringify!(SharpeRatio).to_string()
51 }
52
53 fn calculate_from_returns(&self, raw_returns: &Returns) -> Option<Self::Item> {
54 if !self.check_valid_returns(raw_returns) {
55 return Some(f64::NAN);
56 }
57
58 let returns = self.downsample_to_daily_bins(raw_returns);
59 let mean = returns.values().sum::<f64>() / returns.len() as f64;
60 let std = self.calculate_std(&returns);
61
62 if std < f64::EPSILON {
63 return Some(f64::NAN);
64 }
65
66 let annualized_ratio = (mean / std) * (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 = SharpeRatio::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_std_dev() {
105 let ratio = SharpeRatio::new(None);
106 let returns = create_returns(vec![0.01; 10]);
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_sharpe_ratio() {
114 let ratio = SharpeRatio::new(Some(252));
115 let returns = create_returns(vec![0.01, -0.02, 0.015, -0.005, 0.025]);
116 let result = ratio.calculate_from_returns(&returns);
117 assert!(result.is_some());
118 assert_eq!(result.unwrap(), 4.48998886412873);
119 }
120
121 #[rstest]
122 fn test_name() {
123 let ratio = SharpeRatio::new(None);
124 assert_eq!(ratio.name(), "SharpeRatio");
125 }
126}