nautilus_analysis/statistics/
sharpe_ratio.rs1use std::fmt::{self, Display};
17
18use nautilus_model::position::Position;
19
20use crate::{Returns, statistic::PortfolioStatistic};
21
22#[repr(C)]
36#[derive(Debug, Clone)]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
40)]
41pub struct SharpeRatio {
42 period: usize,
44}
45
46impl SharpeRatio {
47 #[must_use]
49 pub fn new(period: Option<usize>) -> Self {
50 Self {
51 period: period.unwrap_or(252),
52 }
53 }
54}
55
56impl Display for SharpeRatio {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "Sharpe Ratio ({} days)", self.period)
59 }
60}
61
62impl PortfolioStatistic for SharpeRatio {
63 type Item = f64;
64
65 fn name(&self) -> String {
66 self.to_string()
67 }
68
69 fn calculate_from_returns(&self, raw_returns: &Returns) -> Option<Self::Item> {
70 if !self.check_valid_returns(raw_returns) {
71 return Some(f64::NAN);
72 }
73
74 let returns = self.downsample_to_daily_bins(raw_returns);
75 let mean = returns.values().sum::<f64>() / returns.len() as f64;
76 let std = self.calculate_std(&returns);
77
78 if std < f64::EPSILON {
79 return Some(f64::NAN);
80 }
81
82 let annualized_ratio = (mean / std) * (self.period as f64).sqrt();
83
84 Some(annualized_ratio)
85 }
86 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
87 None
88 }
89
90 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
91 None
92 }
93}
94
95#[cfg(test)]
100mod tests {
101 use std::collections::BTreeMap;
102
103 use nautilus_core::{UnixNanos, approx_eq};
104 use rstest::rstest;
105
106 use super::*;
107
108 fn create_returns(values: Vec<f64>) -> BTreeMap<UnixNanos, f64> {
109 let mut new_return = BTreeMap::new();
110 let one_day_in_nanos = 86_400_000_000_000;
111 let start_time = 1_600_000_000_000_000_000;
112
113 for (i, &value) in values.iter().enumerate() {
114 let timestamp = start_time + i as u64 * one_day_in_nanos;
115 new_return.insert(UnixNanos::from(timestamp), value);
116 }
117
118 new_return
119 }
120
121 #[rstest]
122 fn test_empty_returns() {
123 let ratio = SharpeRatio::new(None);
124 let returns = create_returns(vec![]);
125 let result = ratio.calculate_from_returns(&returns);
126 assert!(result.is_some());
127 assert!(result.unwrap().is_nan());
128 }
129
130 #[rstest]
131 fn test_zero_std_dev() {
132 let ratio = SharpeRatio::new(None);
133 let returns = create_returns(vec![0.01; 10]);
134 let result = ratio.calculate_from_returns(&returns);
135 assert!(result.is_some());
136 assert!(result.unwrap().is_nan());
137 }
138
139 #[rstest]
140 fn test_valid_sharpe_ratio() {
141 let ratio = SharpeRatio::new(Some(252));
142 let returns = create_returns(vec![0.01, -0.02, 0.015, -0.005, 0.025]);
143 let result = ratio.calculate_from_returns(&returns);
144 assert!(result.is_some());
145 assert!(approx_eq!(
146 f64,
147 result.unwrap(),
148 4.48998886412873,
149 epsilon = 1e-9
150 ));
151 }
152
153 #[rstest]
154 fn test_name() {
155 let ratio = SharpeRatio::new(None);
156 assert_eq!(ratio.name(), "Sharpe Ratio (252 days)");
157 }
158}