nautilus_analysis/statistics/
sharpe_ratio.rs1use std::fmt::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 std::fmt::Formatter<'_>) -> std::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)]
96mod tests {
97 use std::collections::BTreeMap;
98
99 use nautilus_core::{UnixNanos, approx_eq};
100 use rstest::rstest;
101
102 use super::*;
103
104 fn create_returns(values: Vec<f64>) -> BTreeMap<UnixNanos, f64> {
105 let mut new_return = BTreeMap::new();
106 let one_day_in_nanos = 86_400_000_000_000;
107 let start_time = 1_600_000_000_000_000_000;
108
109 for (i, &value) in values.iter().enumerate() {
110 let timestamp = start_time + i as u64 * one_day_in_nanos;
111 new_return.insert(UnixNanos::from(timestamp), value);
112 }
113
114 new_return
115 }
116
117 #[rstest]
118 fn test_empty_returns() {
119 let ratio = SharpeRatio::new(None);
120 let returns = create_returns(vec![]);
121 let result = ratio.calculate_from_returns(&returns);
122 assert!(result.is_some());
123 assert!(result.unwrap().is_nan());
124 }
125
126 #[rstest]
127 fn test_zero_std_dev() {
128 let ratio = SharpeRatio::new(None);
129 let returns = create_returns(vec![0.01; 10]);
130 let result = ratio.calculate_from_returns(&returns);
131 assert!(result.is_some());
132 assert!(result.unwrap().is_nan());
133 }
134
135 #[rstest]
136 fn test_valid_sharpe_ratio() {
137 let ratio = SharpeRatio::new(Some(252));
138 let returns = create_returns(vec![0.01, -0.02, 0.015, -0.005, 0.025]);
139 let result = ratio.calculate_from_returns(&returns);
140 assert!(result.is_some());
141 assert!(approx_eq!(
142 f64,
143 result.unwrap(),
144 4.48998886412873,
145 epsilon = 1e-9
146 ));
147 }
148
149 #[rstest]
150 fn test_name() {
151 let ratio = SharpeRatio::new(None);
152 assert_eq!(ratio.name(), "Sharpe Ratio (252 days)");
153 }
154}