wickra_core/indicators/
jarque_bera.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
43pub struct JarqueBera {
44 period: usize,
45 window: VecDeque<f64>,
46 last: Option<f64>,
47}
48
49impl JarqueBera {
50 pub fn new(period: usize) -> Result<Self> {
58 if period == 0 {
59 return Err(Error::PeriodZero);
60 }
61 if period > crate::error::MAX_PERIOD {
62 return Err(Error::InvalidPeriod {
63 message: crate::error::PERIOD_ABOVE_MAX,
64 });
65 }
66 if period < 4 {
67 return Err(Error::InvalidPeriod {
68 message: "Jarque-Bera needs period >= 4",
69 });
70 }
71 Ok(Self {
72 period,
73 window: VecDeque::with_capacity(period),
74 last: None,
75 })
76 }
77
78 pub const fn period(&self) -> usize {
80 self.period
81 }
82
83 pub const fn value(&self) -> Option<f64> {
85 self.last
86 }
87
88 fn compute(&self) -> f64 {
89 let n = self.period as f64;
90 let mean = self.window.iter().sum::<f64>() / n;
91 let mut m2 = 0.0;
92 let mut m3 = 0.0;
93 let mut m4 = 0.0;
94 for &v in &self.window {
95 let d = v - mean;
96 let d2 = d * d;
97 m2 += d2;
98 m3 += d2 * d;
99 m4 += d2 * d2;
100 }
101 m2 /= n;
102 m3 /= n;
103 m4 /= n;
104 if m2 == 0.0 {
105 return 0.0;
106 }
107 let skew = m3 / m2.powf(1.5);
108 let excess_kurt = m4 / (m2 * m2) - 3.0;
109 (n / 6.0) * (skew * skew + excess_kurt * excess_kurt / 4.0)
110 }
111}
112
113impl Indicator for JarqueBera {
114 type Input = f64;
115 type Output = f64;
116
117 #[inline]
118 fn update(&mut self, input: f64) -> Option<f64> {
119 if !input.is_finite() {
120 return None;
121 }
122 if self.window.len() == self.period {
123 self.window.pop_front();
124 }
125 self.window.push_back(input);
126 if self.window.len() < self.period {
127 return None;
128 }
129 let out = self.compute();
130 self.last = Some(out);
131 Some(out)
132 }
133
134 fn reset(&mut self) {
135 self.window.clear();
136 self.last = None;
137 }
138
139 #[inline]
140 fn warmup_period(&self) -> usize {
141 self.period
142 }
143
144 #[inline]
145 fn is_ready(&self) -> bool {
146 self.last.is_some()
147 }
148
149 #[inline]
150 fn name(&self) -> &'static str {
151 "JarqueBera"
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::traits::BatchExt;
159 use approx::assert_relative_eq;
160
161 #[test]
162 fn rejects_invalid_period() {
163 assert!(matches!(JarqueBera::new(0), Err(Error::PeriodZero)));
164 assert!(matches!(
165 JarqueBera::new(3),
166 Err(Error::InvalidPeriod { .. })
167 ));
168 assert!(JarqueBera::new(4).is_ok());
169 }
170
171 #[test]
172 fn accessors_and_metadata() {
173 let jb = JarqueBera::new(50).unwrap();
174 assert_eq!(jb.period(), 50);
175 assert_eq!(jb.warmup_period(), 50);
176 assert_eq!(jb.name(), "JarqueBera");
177 assert!(!jb.is_ready());
178 assert_eq!(jb.value(), None);
179 }
180
181 #[test]
182 fn first_emission_at_warmup_period() {
183 let mut jb = JarqueBera::new(4).unwrap();
184 let out = jb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
185 for v in out.iter().take(3) {
186 assert!(v.is_none());
187 }
188 assert!(out[3].is_some());
189 }
190
191 #[test]
192 fn constant_window_is_zero() {
193 let mut jb = JarqueBera::new(8).unwrap();
194 let last = jb.batch(&[5.0; 12]).into_iter().flatten().last().unwrap();
195 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn output_is_non_negative() {
200 let mut jb = JarqueBera::new(30).unwrap();
201 for v in jb
202 .batch(
203 &(0..200)
204 .map(|i| (f64::from(i) * 0.3).sin() * 5.0)
205 .collect::<Vec<_>>(),
206 )
207 .into_iter()
208 .flatten()
209 {
210 assert!(v >= 0.0, "JB must be non-negative, got {v}");
211 }
212 }
213
214 #[test]
215 fn skewed_window_exceeds_symmetric() {
216 let symmetric: Vec<f64> = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.0, 2.0, 0.0];
218 let skewed: Vec<f64> = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0];
219 let jb_sym = JarqueBera::new(8)
220 .unwrap()
221 .batch(&symmetric)
222 .into_iter()
223 .flatten()
224 .last()
225 .unwrap();
226 let jb_skew = JarqueBera::new(8)
227 .unwrap()
228 .batch(&skewed)
229 .into_iter()
230 .flatten()
231 .last()
232 .unwrap();
233 assert!(
234 jb_skew > jb_sym,
235 "skewed ({jb_skew}) should exceed symmetric ({jb_sym})"
236 );
237 }
238
239 #[test]
240 fn ignores_non_finite() {
241 let mut jb = JarqueBera::new(4).unwrap();
242 let _ready = jb
243 .batch(&[1.0, 2.0, 3.0, 5.0])
244 .into_iter()
245 .flatten()
246 .last()
247 .unwrap();
248 assert_eq!(jb.update(f64::NAN), None);
249 }
250
251 #[test]
252 fn reset_clears_state() {
253 let mut jb = JarqueBera::new(4).unwrap();
254 jb.batch(&[1.0, 2.0, 3.0, 5.0]);
255 assert!(jb.is_ready());
256 jb.reset();
257 assert!(!jb.is_ready());
258 assert_eq!(jb.value(), None);
259 assert_eq!(jb.update(1.0), None);
260 }
261
262 #[test]
263 fn batch_equals_streaming() {
264 let xs: Vec<f64> = (0..120)
265 .map(|i| (f64::from(i) * 0.25).sin() * 9.0)
266 .collect();
267 let batch = JarqueBera::new(30).unwrap().batch(&xs);
268 let mut b = JarqueBera::new(30).unwrap();
269 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
270 assert_eq!(batch, streamed);
271 }
272}