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 * m2.sqrt());
110 let excess_kurt = m4 / (m2 * m2) - 3.0;
111 (n / 6.0) * (skew * skew + excess_kurt * excess_kurt / 4.0)
112 }
113}
114
115impl Indicator for JarqueBera {
116 type Input = f64;
117 type Output = f64;
118
119 #[inline]
120 fn update(&mut self, input: f64) -> Option<f64> {
121 if !input.is_finite() {
122 return None;
123 }
124 if self.window.len() == self.period {
125 self.window.pop_front();
126 }
127 self.window.push_back(input);
128 if self.window.len() < self.period {
129 return None;
130 }
131 let out = self.compute();
132 self.last = Some(out);
133 Some(out)
134 }
135
136 fn reset(&mut self) {
137 self.window.clear();
138 self.last = None;
139 }
140
141 #[inline]
142 fn warmup_period(&self) -> usize {
143 self.period
144 }
145
146 #[inline]
147 fn is_ready(&self) -> bool {
148 self.last.is_some()
149 }
150
151 #[inline]
152 fn name(&self) -> &'static str {
153 "JarqueBera"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::traits::BatchExt;
161 use approx::assert_relative_eq;
162
163 #[test]
164 fn rejects_invalid_period() {
165 assert!(matches!(JarqueBera::new(0), Err(Error::PeriodZero)));
166 assert!(matches!(
167 JarqueBera::new(3),
168 Err(Error::InvalidPeriod { .. })
169 ));
170 assert!(JarqueBera::new(4).is_ok());
171 }
172
173 #[test]
174 fn accessors_and_metadata() {
175 let jb = JarqueBera::new(50).unwrap();
176 assert_eq!(jb.period(), 50);
177 assert_eq!(jb.warmup_period(), 50);
178 assert_eq!(jb.name(), "JarqueBera");
179 assert!(!jb.is_ready());
180 assert_eq!(jb.value(), None);
181 }
182
183 #[test]
184 fn first_emission_at_warmup_period() {
185 let mut jb = JarqueBera::new(4).unwrap();
186 let out = jb.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
187 for v in out.iter().take(3) {
188 assert!(v.is_none());
189 }
190 assert!(out[3].is_some());
191 }
192
193 #[test]
194 fn constant_window_is_zero() {
195 let mut jb = JarqueBera::new(8).unwrap();
196 let last = jb.batch(&[5.0; 12]).into_iter().flatten().last().unwrap();
197 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
198 }
199
200 #[test]
201 fn output_is_non_negative() {
202 let mut jb = JarqueBera::new(30).unwrap();
203 for v in jb
204 .batch(
205 &(0..200)
206 .map(|i| (f64::from(i) * 0.3).sin() * 5.0)
207 .collect::<Vec<_>>(),
208 )
209 .into_iter()
210 .flatten()
211 {
212 assert!(v >= 0.0, "JB must be non-negative, got {v}");
213 }
214 }
215
216 #[test]
217 fn skewed_window_exceeds_symmetric() {
218 let symmetric: Vec<f64> = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.0, 2.0, 0.0];
220 let skewed: Vec<f64> = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0];
221 let jb_sym = JarqueBera::new(8)
222 .unwrap()
223 .batch(&symmetric)
224 .into_iter()
225 .flatten()
226 .last()
227 .unwrap();
228 let jb_skew = JarqueBera::new(8)
229 .unwrap()
230 .batch(&skewed)
231 .into_iter()
232 .flatten()
233 .last()
234 .unwrap();
235 assert!(
236 jb_skew > jb_sym,
237 "skewed ({jb_skew}) should exceed symmetric ({jb_sym})"
238 );
239 }
240
241 #[test]
242 fn ignores_non_finite() {
243 let mut jb = JarqueBera::new(4).unwrap();
244 let _ready = jb
245 .batch(&[1.0, 2.0, 3.0, 5.0])
246 .into_iter()
247 .flatten()
248 .last()
249 .unwrap();
250 assert_eq!(jb.update(f64::NAN), None);
251 }
252
253 #[test]
254 fn reset_clears_state() {
255 let mut jb = JarqueBera::new(4).unwrap();
256 jb.batch(&[1.0, 2.0, 3.0, 5.0]);
257 assert!(jb.is_ready());
258 jb.reset();
259 assert!(!jb.is_ready());
260 assert_eq!(jb.value(), None);
261 assert_eq!(jb.update(1.0), None);
262 }
263
264 #[test]
265 fn batch_equals_streaming() {
266 let xs: Vec<f64> = (0..120)
267 .map(|i| (f64::from(i) * 0.25).sin() * 9.0)
268 .collect();
269 let batch = JarqueBera::new(30).unwrap().batch(&xs);
270 let mut b = JarqueBera::new(30).unwrap();
271 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
272 assert_eq!(batch, streamed);
273 }
274}