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