wickra_core/indicators/
bipower_variation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
51pub struct BipowerVariation {
52 period: usize,
53 prev_price: Option<f64>,
54 window: VecDeque<f64>,
56 sum_adjacent: RollingSum,
58 last: Option<f64>,
59}
60
61impl BipowerVariation {
62 pub fn new(period: usize) -> Result<Self> {
72 if period == 0 {
73 return Err(Error::PeriodZero);
74 }
75 if period > crate::error::MAX_PERIOD {
76 return Err(Error::InvalidPeriod {
77 message: crate::error::PERIOD_ABOVE_MAX,
78 });
79 }
80 if period < 2 {
81 return Err(Error::InvalidPeriod {
82 message: "bipower variation period must be >= 2",
83 });
84 }
85 Ok(Self {
86 period,
87 prev_price: None,
88 window: VecDeque::with_capacity(period),
89 sum_adjacent: RollingSum::new(),
90 last: None,
91 })
92 }
93
94 pub const fn period(&self) -> usize {
96 self.period
97 }
98}
99
100const MU1_INV_SQ: f64 = std::f64::consts::FRAC_PI_2;
102
103impl Indicator for BipowerVariation {
104 type Input = f64;
105 type Output = f64;
106
107 #[inline]
108 fn update(&mut self, input: f64) -> Option<f64> {
109 if !input.is_finite() || input <= 0.0 {
112 return None;
113 }
114 let Some(prev) = self.prev_price else {
115 self.prev_price = Some(input);
116 return None;
117 };
118 self.prev_price = Some(input);
119 let r = (input / prev).ln();
122 if let Some(&back) = self.window.back() {
124 self.sum_adjacent.push(back.abs() * r.abs());
125 }
126 self.window.push_back(r);
127 if self.window.len() > self.period {
128 let first = self.window.pop_front().expect("window is non-empty");
129 let second = *self.window.front().expect("window still has >= 1 element");
131 self.sum_adjacent.evict(first.abs() * second.abs());
132 if self.sum_adjacent.needs_reseed(self.period) {
133 self.sum_adjacent.reseed(
134 self.window
135 .iter()
136 .zip(self.window.iter().skip(1))
137 .map(|(a, b)| a.abs() * b.abs()),
138 );
139 }
140 }
141 if self.window.len() < self.period {
142 return None;
143 }
144 let bv = MU1_INV_SQ * self.sum_adjacent.value().max(0.0);
147 self.last = Some(bv);
148 Some(bv)
149 }
150
151 fn reset(&mut self) {
152 self.prev_price = None;
153 self.window.clear();
154 self.sum_adjacent.reset();
155 self.last = None;
156 }
157
158 #[inline]
159 fn warmup_period(&self) -> usize {
160 self.period + 1
162 }
163
164 #[inline]
165 fn is_ready(&self) -> bool {
166 self.last.is_some()
167 }
168
169 #[inline]
170 fn name(&self) -> &'static str {
171 "BipowerVariation"
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::traits::BatchExt;
179 use approx::assert_relative_eq;
180
181 #[test]
182 fn rejects_zero_period() {
183 assert!(matches!(BipowerVariation::new(0), Err(Error::PeriodZero)));
184 }
185
186 #[test]
187 fn rejects_period_one() {
188 assert!(matches!(
189 BipowerVariation::new(1),
190 Err(Error::InvalidPeriod { .. })
191 ));
192 }
193
194 #[test]
195 fn accessors_and_metadata() {
196 let bv = BipowerVariation::new(20).unwrap();
197 assert_eq!(bv.period(), 20);
198 assert_eq!(bv.warmup_period(), 21);
199 assert_eq!(bv.name(), "BipowerVariation");
200 assert!(!bv.is_ready());
201 }
202
203 #[test]
204 fn first_emission_at_warmup_period() {
205 let mut bv = BipowerVariation::new(5).unwrap();
206 let out = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
207 for v in out.iter().take(5) {
208 assert!(v.is_none());
209 }
210 assert!(out[5].is_some());
211 }
212
213 #[test]
214 fn known_value() {
215 let mut bv = BipowerVariation::new(2).unwrap();
218 let out = bv.batch(&[100.0, 110.0, 99.0]);
219 assert!(out[1].is_none());
220 let r1 = (110.0_f64 / 100.0).ln();
221 let r2 = (99.0_f64 / 110.0).ln();
222 let expected = std::f64::consts::FRAC_PI_2 * r1.abs() * r2.abs();
223 assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-12);
224 }
225
226 #[test]
227 fn rolling_window_drops_oldest_product() {
228 let mut bv = BipowerVariation::new(2).unwrap();
230 let out = bv.batch(&[100.0, 110.0, 99.0, 105.0]);
231 let r2 = (99.0_f64 / 110.0).ln();
232 let r3 = (105.0_f64 / 99.0).ln();
233 let expected = std::f64::consts::FRAC_PI_2 * r2.abs() * r3.abs();
234 assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-12);
235 }
236
237 #[test]
238 fn constant_series_yields_zero() {
239 let mut bv = BipowerVariation::new(10).unwrap();
240 for v in bv.batch(&[100.0; 40]).into_iter().flatten() {
241 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
242 }
243 }
244
245 #[test]
246 fn output_is_non_negative() {
247 let mut bv = BipowerVariation::new(20).unwrap();
248 let prices: Vec<f64> = (1..=200)
249 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
250 .collect();
251 for v in bv.batch(&prices).into_iter().flatten() {
252 assert!(v >= 0.0, "bipower variation must be non-negative, got {v}");
253 }
254 }
255
256 #[test]
257 fn ignores_non_finite_input() {
258 let mut bv = BipowerVariation::new(5).unwrap();
259 let out = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
260 let last = *out.last().unwrap();
261 assert!(last.is_some());
262 assert_eq!(bv.update(f64::NAN), None);
263 assert_eq!(bv.update(f64::INFINITY), None);
264 }
265
266 #[test]
267 fn skips_non_positive_prices() {
268 let mut bv = BipowerVariation::new(5).unwrap();
269 let warmup = bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
270 warmup.last().copied().flatten().expect("warmed up");
271 assert_eq!(bv.update(-5.0), None);
272 assert_eq!(bv.update(0.0), None);
273 let mut control = bv.clone();
275 let after = bv.update(21.0).expect("ready");
276 assert_eq!(control.update(21.0).expect("ready"), after);
277 }
278
279 #[test]
280 fn reset_clears_state() {
281 let mut bv = BipowerVariation::new(5).unwrap();
282 bv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
283 assert!(bv.is_ready());
284 bv.reset();
285 assert!(!bv.is_ready());
286 assert_eq!(bv.update(1.0), None);
287 }
288
289 #[test]
290 fn batch_equals_streaming() {
291 let prices: Vec<f64> = (1..=120)
292 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
293 .collect();
294 let batch = BipowerVariation::new(20).unwrap().batch(&prices);
295 let mut b = BipowerVariation::new(20).unwrap();
296 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
297 assert_eq!(batch, streamed);
298 }
299}