nautilus_analysis/statistics/
returns_kurtosis.rs1use nautilus_model::position::Position;
19
20use crate::{Returns, statistic::PortfolioStatistic};
21
22#[repr(C)]
42#[derive(Debug, Clone, Default)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.analysis", from_py_object)
46)]
47#[cfg_attr(
48 feature = "python",
49 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
50)]
51pub struct ReturnsKurtosis {}
52
53impl ReturnsKurtosis {
54 #[must_use]
56 pub fn new() -> Self {
57 Self {}
58 }
59}
60
61impl PortfolioStatistic for ReturnsKurtosis {
62 type Item = f64;
63
64 fn name(&self) -> String {
65 "Returns Kurtosis".to_string()
66 }
67
68 fn calculate_from_returns(&self, raw_returns: &Returns) -> Option<Self::Item> {
69 if !self.check_valid_returns(raw_returns) {
70 return Some(f64::NAN);
71 }
72
73 let returns = self.downsample_to_daily_bins(raw_returns);
74 let n = returns.len();
75 if n < 4 {
76 return Some(f64::NAN);
77 }
78
79 let n_f = n as f64;
80 let mean = returns.values().sum::<f64>() / n_f;
81 let std = self.calculate_std(&returns);
82 if std == 0.0 || !std.is_finite() {
83 return Some(f64::NAN);
84 }
85
86 let sum_quartic = returns
87 .values()
88 .map(|x| ((x - mean) / std).powi(4))
89 .sum::<f64>();
90 let kurtosis = (n_f * (n_f + 1.0)) / ((n_f - 1.0) * (n_f - 2.0) * (n_f - 3.0))
91 * sum_quartic
92 - 3.0 * (n_f - 1.0).powi(2) / ((n_f - 2.0) * (n_f - 3.0));
93
94 Some(kurtosis)
95 }
96
97 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
98 None
99 }
100
101 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
102 None
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use std::collections::BTreeMap;
109
110 use nautilus_core::{UnixNanos, approx_eq};
111 use rstest::rstest;
112
113 use super::*;
114
115 fn create_returns(values: &[f64]) -> BTreeMap<UnixNanos, f64> {
116 let mut new_return = BTreeMap::new();
117 let one_day_in_nanos = 86_400_000_000_000;
118 let start_time = 1_600_000_000_000_000_000;
119
120 for (i, &value) in values.iter().enumerate() {
121 let timestamp = start_time + i as u64 * one_day_in_nanos;
122 new_return.insert(UnixNanos::from(timestamp), value);
123 }
124
125 new_return
126 }
127
128 #[rstest]
129 fn test_name() {
130 let kurtosis = ReturnsKurtosis::new();
131 assert_eq!(kurtosis.name(), "Returns Kurtosis");
132 }
133
134 #[rstest]
135 fn test_empty_returns() {
136 let kurtosis = ReturnsKurtosis::new();
137 let returns = create_returns(&[]);
138 let result = kurtosis.calculate_from_returns(&returns);
139 assert!(result.is_some());
140 assert!(result.unwrap().is_nan());
141 }
142
143 #[rstest]
144 fn test_insufficient_data() {
145 let kurtosis = ReturnsKurtosis::new();
146 let returns = create_returns(&[0.01, -0.02, 0.03]);
147 let result = kurtosis.calculate_from_returns(&returns);
148 assert!(result.is_some());
149 assert!(result.unwrap().is_nan());
150 }
151
152 #[rstest]
153 fn test_zero_dispersion() {
154 let kurtosis = ReturnsKurtosis::new();
155 let returns = create_returns(&[0.01, 0.01, 0.01, 0.01]);
156 let result = kurtosis.calculate_from_returns(&returns);
157 assert!(result.is_some());
158 assert!(result.unwrap().is_nan());
159 }
160
161 #[rstest]
162 fn test_kurtosis_calculation() {
163 let kurtosis = ReturnsKurtosis::new();
165 let returns = create_returns(&[
166 0.01, -0.02, 0.03, -0.01, 0.02, 0.04, -0.03, 0.05, -0.04, 0.02,
167 ]);
168 let result = kurtosis.calculate_from_returns(&returns);
169 assert!(result.is_some());
170 assert!(approx_eq!(
171 f64,
172 result.unwrap(),
173 -1.2622443251995028,
174 epsilon = 1e-12
175 ));
176 }
177}