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