wickra_core/indicators/
skewness.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedHigherMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
44pub struct Skewness {
45 period: usize,
46 window: VecDeque<f64>,
47 moments: ShiftedHigherMoments,
48}
49
50impl Skewness {
51 pub fn new(period: usize) -> Result<Self> {
56 if period < 3 {
57 return Err(Error::InvalidPeriod {
58 message: "skewness needs period >= 3",
59 });
60 }
61 if period > crate::error::MAX_PERIOD {
62 return Err(Error::InvalidPeriod {
63 message: crate::error::PERIOD_ABOVE_MAX,
64 });
65 }
66 Ok(Self {
67 period,
68 window: VecDeque::with_capacity(period),
69 moments: ShiftedHigherMoments::new(),
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77}
78
79impl Indicator for Skewness {
80 type Input = f64;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, value: f64) -> Option<f64> {
85 if !value.is_finite() {
86 return None;
87 }
88 if self.window.len() == self.period {
89 let old = self.window.pop_front().expect("non-empty");
90 self.moments.evict(old);
91 }
92 self.window.push_back(value);
93 self.moments.push(value);
94 if self.moments.needs_reseed(self.period) {
95 self.moments.reseed(self.window.iter().copied());
96 }
97 if self.window.len() < self.period {
98 return None;
99 }
100 let m2 = self.moments.m2(self.period);
101 let m3 = self.moments.m3(self.period);
102 if m2 == 0.0 {
103 return Some(0.0);
105 }
106 Some(m3 / m2.powf(1.5))
107 }
108
109 fn reset(&mut self) {
110 self.window.clear();
111 self.moments.reset();
112 }
113
114 #[inline]
115 fn warmup_period(&self) -> usize {
116 self.period
117 }
118
119 #[inline]
120 fn is_ready(&self) -> bool {
121 self.window.len() == self.period
122 }
123
124 #[inline]
125 fn name(&self) -> &'static str {
126 "Skewness"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134 use approx::assert_relative_eq;
135
136 #[test]
137 fn rejects_period_below_three() {
138 assert!(Skewness::new(0).is_err());
139 assert!(Skewness::new(1).is_err());
140 assert!(Skewness::new(2).is_err());
141 assert!(Skewness::new(3).is_ok());
142 }
143
144 #[test]
145 fn accessors_and_metadata() {
146 let s = Skewness::new(14).unwrap();
147 assert_eq!(s.period(), 14);
148 assert_eq!(s.warmup_period(), 14);
149 assert_eq!(s.name(), "Skewness");
150 }
151
152 #[test]
153 fn symmetric_window_is_zero() {
154 let mut s = Skewness::new(5).unwrap();
156 let out = s.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
157 assert_relative_eq!(out[4].unwrap(), 0.0, epsilon = 1e-9);
158 }
159
160 #[test]
161 fn constant_series_yields_zero() {
162 let mut s = Skewness::new(5).unwrap();
163 for v in s.batch(&[42.0; 20]).into_iter().flatten() {
164 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
165 }
166 }
167
168 #[test]
169 fn right_tail_is_positive() {
170 let mut s = Skewness::new(5).unwrap();
172 let out = s.batch(&[0.0, 0.0, 0.0, 0.0, 10.0]);
173 assert!(out[4].unwrap() > 0.0);
174 }
175
176 #[test]
177 fn left_tail_is_negative() {
178 let mut s = Skewness::new(5).unwrap();
180 let out = s.batch(&[10.0, 10.0, 10.0, 10.0, 0.0]);
181 assert!(out[4].unwrap() < 0.0);
182 }
183
184 #[test]
185 fn reset_clears_state() {
186 let mut s = Skewness::new(5).unwrap();
187 s.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
188 assert!(s.is_ready());
189 s.reset();
190 assert!(!s.is_ready());
191 assert_eq!(s.update(1.0), None);
192 }
193
194 #[test]
195 fn batch_equals_streaming() {
196 let prices: Vec<f64> = (0..60)
197 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 7.0)
198 .collect();
199 let batch = Skewness::new(14).unwrap().batch(&prices);
200 let mut b = Skewness::new(14).unwrap();
201 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
202 assert_eq!(batch, streamed);
203 }
204}