nautilus_analysis/statistics/
alpha.rs1use std::fmt::Display;
19
20use nautilus_model::position::Position;
21
22use crate::{Returns, statistic::PortfolioStatistic, statistics::beta_ratio::beta};
23
24#[repr(C)]
44#[derive(Debug, Clone)]
45#[cfg_attr(
46 feature = "python",
47 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
48)]
49#[cfg_attr(
50 feature = "python",
51 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
52)]
53pub struct Alpha {
54 period: usize,
56 risk_free_rate: f64,
58}
59
60impl Alpha {
61 #[must_use]
63 pub fn new(period: Option<usize>, risk_free_rate: Option<f64>) -> Self {
64 Self {
65 period: period.unwrap_or(252),
66 risk_free_rate: risk_free_rate.unwrap_or(0.0),
67 }
68 }
69}
70
71impl Display for Alpha {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "Alpha ({} days)", self.period)
74 }
75}
76
77impl PortfolioStatistic for Alpha {
78 type Item = f64;
79
80 fn name(&self) -> String {
81 self.to_string()
82 }
83
84 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
85 None
86 }
87
88 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
89 None
90 }
91
92 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
93 None
94 }
95
96 fn calculate_from_returns_with_benchmark(
97 &self,
98 returns: &Returns,
99 benchmark: &Returns,
100 ) -> Option<Self::Item> {
101 let (r, b) = self.align_returns(returns, benchmark);
102 let n = r.len();
103 if n < 2 {
104 return Some(f64::NAN);
105 }
106
107 let beta = beta(&r, &b);
108 if beta.is_nan() {
109 return Some(f64::NAN);
110 }
111
112 let mean_r = r.iter().sum::<f64>() / n as f64;
113 let mean_b = b.iter().sum::<f64>() / n as f64;
114 let rf = self.risk_free_rate;
115
116 let alpha_period = (mean_r - rf) - beta * (mean_b - rf);
117 let alpha_annual = (1.0 + alpha_period).powf(self.period as f64) - 1.0;
118
119 Some(alpha_annual)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use std::collections::BTreeMap;
126
127 use nautilus_core::{UnixNanos, approx_eq};
128 use rstest::rstest;
129
130 use super::*;
131
132 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
133 let mut new_return = BTreeMap::new();
134 let one_day_in_nanos = 86_400_000_000_000;
135 let start_time = 1_600_000_000_000_000_000;
136
137 for (i, &value) in values.iter().enumerate() {
138 let timestamp = start_time + i as u64 * one_day_in_nanos;
139 new_return.insert(UnixNanos::from(timestamp), value);
140 }
141
142 new_return
143 }
144
145 #[rstest]
146 fn test_name() {
147 let stat = Alpha::new(None, None);
148 assert_eq!(stat.name(), "Alpha (252 days)");
149 }
150
151 #[rstest]
152 fn test_name_non_default_period() {
153 let stat = Alpha::new(Some(4), None);
154 assert_eq!(stat.name(), "Alpha (4 days)");
155 }
156
157 #[rstest]
158 fn test_known_value_zero_alpha() {
159 let benchmark = create_returns(&[0.01, -0.02, 0.015, -0.005, 0.025]);
163 let returns = create_returns(&[0.02, -0.04, 0.030, -0.010, 0.050]);
164 let stat = Alpha::new(Some(252), Some(0.0));
165 let result = stat
166 .calculate_from_returns_with_benchmark(&returns, &benchmark)
167 .unwrap();
168 assert!(approx_eq!(f64, result, 0.0, epsilon = 1e-12));
169 }
170
171 #[rstest]
172 fn test_known_value_constant_offset() {
173 let benchmark = create_returns(&[0.01, -0.02, 0.015, -0.005]);
176 let returns = create_returns(&[0.011, -0.019, 0.016, -0.004]);
177 let stat = Alpha::new(Some(4), Some(0.0));
178 let result = stat
179 .calculate_from_returns_with_benchmark(&returns, &benchmark)
180 .unwrap();
181 let expected = 1.001_f64.powf(4.0) - 1.0;
182 assert!(approx_eq!(f64, result, expected, epsilon = 1e-12));
183 }
184
185 #[rstest]
186 fn test_known_value_nonzero_mean_benchmark() {
187 let returns = create_returns(&[0.02, -0.01, 0.03, 0.005]);
200 let benchmark = create_returns(&[0.01, 0.0, 0.015, 0.01]);
201 let stat = Alpha::new(Some(4), Some(0.001));
202 let result = stat
203 .calculate_from_returns_with_benchmark(&returns, &benchmark)
204 .unwrap();
205 assert!(approx_eq!(f64, result, -0.0383822153156389, epsilon = 1e-9));
206 }
207
208 #[rstest]
209 fn test_flat_benchmark_is_nan() {
210 let benchmark = create_returns(&[0.01, 0.01, 0.01, 0.01, 0.01]);
211 let returns = create_returns(&[0.02, -0.04, 0.030, -0.010, 0.050]);
212 let stat = Alpha::new(None, None);
213 let result = stat
214 .calculate_from_returns_with_benchmark(&returns, &benchmark)
215 .unwrap();
216 assert!(result.is_nan());
217 }
218
219 #[rstest]
220 fn test_empty_returns_is_nan() {
221 let stat = Alpha::new(None, None);
222 let result = stat
223 .calculate_from_returns_with_benchmark(&create_returns(&[]), &create_returns(&[]))
224 .unwrap();
225 assert!(result.is_nan());
226 }
227
228 #[rstest]
229 fn test_single_overlap_is_nan() {
230 let benchmark = create_returns(&[0.01, -0.02, 0.015]);
231 let returns = create_returns(&[0.02]);
232 let stat = Alpha::new(None, None);
233 let result = stat
234 .calculate_from_returns_with_benchmark(&returns, &benchmark)
235 .unwrap();
236 assert!(result.is_nan());
237 }
238}