nautilus_analysis/statistics/
treynor_ratio.rs1use std::fmt::Display;
19
20use nautilus_model::position::Position;
21
22use crate::{Returns, statistic::PortfolioStatistic, statistics::beta_ratio::beta};
23
24#[repr(C)]
43#[derive(Debug, Clone)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
51)]
52pub struct TreynorRatio {
53 period: usize,
55 risk_free_rate: f64,
57}
58
59impl TreynorRatio {
60 #[must_use]
62 pub fn new(period: Option<usize>, risk_free_rate: Option<f64>) -> Self {
63 Self {
64 period: period.unwrap_or(252),
65 risk_free_rate: risk_free_rate.unwrap_or(0.0),
66 }
67 }
68}
69
70impl Display for TreynorRatio {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(f, "Treynor Ratio ({} days)", self.period)
73 }
74}
75
76impl PortfolioStatistic for TreynorRatio {
77 type Item = f64;
78
79 fn name(&self) -> String {
80 self.to_string()
81 }
82
83 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
84 None
85 }
86
87 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
88 None
89 }
90
91 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
92 None
93 }
94
95 fn calculate_from_returns_with_benchmark(
96 &self,
97 returns: &Returns,
98 benchmark: &Returns,
99 ) -> Option<Self::Item> {
100 let (r, b) = self.align_returns(returns, benchmark);
101 let n = r.len();
102 if n < 2 {
103 return Some(f64::NAN);
104 }
105
106 let beta = beta(&r, &b);
107 if beta.is_nan() || beta.abs() < f64::EPSILON {
108 return Some(f64::NAN);
109 }
110
111 let period = self.period as f64;
112 let growth = r.iter().map(|&ri| 1.0 + ri).product::<f64>();
113 let annualized_return = growth.powf(period / n as f64) - 1.0;
114 let rf_annual = (1.0 + self.risk_free_rate).powf(period) - 1.0;
115
116 Some((annualized_return - rf_annual) / beta)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::collections::BTreeMap;
123
124 use nautilus_core::{UnixNanos, approx_eq};
125 use rstest::rstest;
126
127 use super::*;
128
129 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
130 let mut new_return = BTreeMap::new();
131 let one_day_in_nanos = 86_400_000_000_000;
132 let start_time = 1_600_000_000_000_000_000;
133
134 for (i, &value) in values.iter().enumerate() {
135 let timestamp = start_time + i as u64 * one_day_in_nanos;
136 new_return.insert(UnixNanos::from(timestamp), value);
137 }
138
139 new_return
140 }
141
142 #[rstest]
143 fn test_name() {
144 let stat = TreynorRatio::new(None, None);
145 assert_eq!(stat.name(), "Treynor Ratio (252 days)");
146 }
147
148 #[rstest]
149 fn test_name_non_default_period() {
150 let stat = TreynorRatio::new(Some(63), None);
151 assert_eq!(stat.name(), "Treynor Ratio (63 days)");
152 }
153
154 #[rstest]
155 fn test_known_value() {
156 let benchmark = create_returns(&[0.01, -0.02, 0.015, -0.005, 0.025]);
161 let returns = create_returns(&[0.02, -0.04, 0.030, -0.010, 0.050]);
162 let stat = TreynorRatio::new(Some(5), Some(0.0));
163 let result = stat
164 .calculate_from_returns_with_benchmark(&returns, &benchmark)
165 .unwrap();
166
167 let growth = 1.02_f64 * 0.96 * 1.03 * 0.99 * 1.05;
168 let expected = (growth - 1.0) / 2.0;
169 assert!(approx_eq!(f64, result, expected, epsilon = 1e-12));
170 }
171
172 #[rstest]
173 fn test_known_value_period_ne_n_and_nonzero_rf() {
174 let returns = create_returns(&[0.02, -0.01, 0.03, 0.005]);
185 let benchmark = create_returns(&[0.01, 0.0, 0.015, 0.01]);
186 let stat = TreynorRatio::new(Some(252), Some(0.0001));
187 let result = stat
188 .calculate_from_returns_with_benchmark(&returns, &benchmark)
189 .unwrap();
190 assert!(approx_eq!(f64, result, 5.920539327065061, epsilon = 1e-9));
191 }
192
193 #[rstest]
194 fn test_flat_benchmark_is_nan() {
195 let benchmark = create_returns(&[0.01, 0.01, 0.01, 0.01, 0.01]);
197 let returns = create_returns(&[0.02, -0.04, 0.030, -0.010, 0.050]);
198 let stat = TreynorRatio::new(None, None);
199 let result = stat
200 .calculate_from_returns_with_benchmark(&returns, &benchmark)
201 .unwrap();
202 assert!(result.is_nan());
203 }
204
205 #[rstest]
206 fn test_zero_beta_is_nan() {
207 let returns = create_returns(&[0.01, -0.01, 0.01, -0.01]);
213 let benchmark = create_returns(&[0.01, 0.01, -0.01, -0.01]);
214 let stat = TreynorRatio::new(None, None);
215 let result = stat
216 .calculate_from_returns_with_benchmark(&returns, &benchmark)
217 .unwrap();
218 assert!(result.is_nan());
219 }
220
221 #[rstest]
222 fn test_empty_returns_is_nan() {
223 let stat = TreynorRatio::new(None, None);
224 let result = stat
225 .calculate_from_returns_with_benchmark(&create_returns(&[]), &create_returns(&[]))
226 .unwrap();
227 assert!(result.is_nan());
228 }
229
230 #[rstest]
231 fn test_single_overlap_is_nan() {
232 let benchmark = create_returns(&[0.01, -0.02, 0.015]);
233 let returns = create_returns(&[0.02]);
234 let stat = TreynorRatio::new(None, None);
235 let result = stat
236 .calculate_from_returns_with_benchmark(&returns, &benchmark)
237 .unwrap();
238 assert!(result.is_nan());
239 }
240}