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