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 * m2.sqrt()))
110 }
111
112 fn reset(&mut self) {
113 self.window.clear();
114 self.moments.reset();
115 }
116
117 #[inline]
118 fn warmup_period(&self) -> usize {
119 self.period
120 }
121
122 #[inline]
123 fn is_ready(&self) -> bool {
124 self.window.len() == self.period
125 }
126
127 #[inline]
128 fn name(&self) -> &'static str {
129 "Skewness"
130 }
131
132 fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
140 assert_eq!(
141 inputs.len(),
142 out.len(),
143 "batch output length must equal input length"
144 );
145 let p = self.period;
146 if !self.window.is_empty() || inputs.len() < p || !crate::fast::in_range(inputs) {
147 self.batch_nan_into(inputs, out);
148 return;
149 }
150 crate::fast::with_scratch(crate::fast::power_scratch_len(3, p), |scratch| {
151 wickra_simd::dispatch(crate::fast::SkewnessFast {
152 x: inputs,
153 period: p,
154 scratch,
155 out,
156 _borrow: std::marker::PhantomData,
157 });
158 });
159 crate::fast::replay_tail(self, &inputs[inputs.len() - p..]);
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::traits::BatchExt;
167 use approx::assert_relative_eq;
168
169 #[test]
170 fn rejects_period_below_three() {
171 assert!(Skewness::new(0).is_err());
172 assert!(Skewness::new(1).is_err());
173 assert!(Skewness::new(2).is_err());
174 assert!(Skewness::new(3).is_ok());
175 }
176
177 #[test]
178 fn accessors_and_metadata() {
179 let s = Skewness::new(14).unwrap();
180 assert_eq!(s.period(), 14);
181 assert_eq!(s.warmup_period(), 14);
182 assert_eq!(s.name(), "Skewness");
183 }
184
185 #[test]
186 fn symmetric_window_is_zero() {
187 let mut s = Skewness::new(5).unwrap();
189 let out = s.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
190 assert_relative_eq!(out[4].unwrap(), 0.0, epsilon = 1e-9);
191 }
192
193 #[test]
194 fn constant_series_yields_zero() {
195 let mut s = Skewness::new(5).unwrap();
196 for v in s.batch(&[42.0; 20]).into_iter().flatten() {
197 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
198 }
199 }
200
201 #[test]
202 fn right_tail_is_positive() {
203 let mut s = Skewness::new(5).unwrap();
205 let out = s.batch(&[0.0, 0.0, 0.0, 0.0, 10.0]);
206 assert!(out[4].unwrap() > 0.0);
207 }
208
209 #[test]
210 fn left_tail_is_negative() {
211 let mut s = Skewness::new(5).unwrap();
213 let out = s.batch(&[10.0, 10.0, 10.0, 10.0, 0.0]);
214 assert!(out[4].unwrap() < 0.0);
215 }
216
217 #[test]
218 fn reset_clears_state() {
219 let mut s = Skewness::new(5).unwrap();
220 s.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
221 assert!(s.is_ready());
222 s.reset();
223 assert!(!s.is_ready());
224 assert_eq!(s.update(1.0), None);
225 }
226
227 #[test]
228 fn batch_equals_streaming() {
229 let prices: Vec<f64> = (0..60)
230 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 7.0)
231 .collect();
232 let batch = Skewness::new(14).unwrap().batch(&prices);
233 let mut b = Skewness::new(14).unwrap();
234 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
235 assert_eq!(batch, streamed);
236 }
237}