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 self.window.len() == self.period {
83 let old = self.window.pop_front().expect("non-empty");
84 self.sum -= old;
85 self.sum_sq -= old * old;
86 self.sum_cu -= old * old * old;
87 }
88 self.window.push_back(value);
89 self.sum += value;
90 self.sum_sq += value * value;
91 self.sum_cu += value * value * value;
92 if self.window.len() < self.period {
93 return None;
94 }
95 let n = self.period as f64;
96 let mean = self.sum / n;
97 let m2 = (self.sum_sq / n - mean * mean).max(0.0);
99 let m3 = self.sum_cu / n - 3.0 * mean * (self.sum_sq / n) + 2.0 * mean * mean * mean;
101 if m2 == 0.0 {
102 return Some(0.0);
104 }
105 Some(m3 / m2.powf(1.5))
106 }
107
108 fn reset(&mut self) {
109 self.window.clear();
110 self.sum = 0.0;
111 self.sum_sq = 0.0;
112 self.sum_cu = 0.0;
113 }
114
115 fn warmup_period(&self) -> usize {
116 self.period
117 }
118
119 fn is_ready(&self) -> bool {
120 self.window.len() == self.period
121 }
122
123 fn name(&self) -> &'static str {
124 "Skewness"
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::traits::BatchExt;
132 use approx::assert_relative_eq;
133
134 #[test]
135 fn rejects_period_below_three() {
136 assert!(Skewness::new(0).is_err());
137 assert!(Skewness::new(1).is_err());
138 assert!(Skewness::new(2).is_err());
139 assert!(Skewness::new(3).is_ok());
140 }
141
142 #[test]
143 fn accessors_and_metadata() {
144 let s = Skewness::new(14).unwrap();
145 assert_eq!(s.period(), 14);
146 assert_eq!(s.warmup_period(), 14);
147 assert_eq!(s.name(), "Skewness");
148 }
149
150 #[test]
151 fn symmetric_window_is_zero() {
152 let mut s = Skewness::new(5).unwrap();
154 let out = s.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
155 assert_relative_eq!(out[4].unwrap(), 0.0, epsilon = 1e-9);
156 }
157
158 #[test]
159 fn constant_series_yields_zero() {
160 let mut s = Skewness::new(5).unwrap();
161 for v in s.batch(&[42.0; 20]).into_iter().flatten() {
162 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
163 }
164 }
165
166 #[test]
167 fn right_tail_is_positive() {
168 let mut s = Skewness::new(5).unwrap();
170 let out = s.batch(&[0.0, 0.0, 0.0, 0.0, 10.0]);
171 assert!(out[4].unwrap() > 0.0);
172 }
173
174 #[test]
175 fn left_tail_is_negative() {
176 let mut s = Skewness::new(5).unwrap();
178 let out = s.batch(&[10.0, 10.0, 10.0, 10.0, 0.0]);
179 assert!(out[4].unwrap() < 0.0);
180 }
181
182 #[test]
183 fn reset_clears_state() {
184 let mut s = Skewness::new(5).unwrap();
185 s.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
186 assert!(s.is_ready());
187 s.reset();
188 assert!(!s.is_ready());
189 assert_eq!(s.update(1.0), None);
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let prices: Vec<f64> = (0..60)
195 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 7.0)
196 .collect();
197 let batch = Skewness::new(14).unwrap().batch(&prices);
198 let mut b = Skewness::new(14).unwrap();
199 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
200 assert_eq!(batch, streamed);
201 }
202}