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