wickra_core/indicators/
variance_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
51pub struct VarianceRatio {
52 period: usize,
53 q: usize,
54 window: VecDeque<f64>,
55 scratch: Vec<f64>,
57 returns: Vec<f64>,
59}
60
61impl VarianceRatio {
62 pub fn new(period: usize, q: usize) -> Result<Self> {
71 if q < 2 {
72 return Err(Error::InvalidPeriod {
73 message: "variance ratio needs q >= 2",
74 });
75 }
76 if q > crate::error::MAX_PERIOD {
77 return Err(Error::InvalidPeriod {
78 message: crate::error::PERIOD_ABOVE_MAX,
79 });
80 }
81 if period < q + 2 {
82 return Err(Error::InvalidPeriod {
83 message: "variance ratio needs period >= q + 2",
84 });
85 }
86 Ok(Self {
87 period,
88 q,
89 window: VecDeque::with_capacity(period),
90 scratch: Vec::with_capacity(period),
91 returns: Vec::with_capacity(period),
92 })
93 }
94
95 pub const fn period(&self) -> usize {
97 self.period
98 }
99
100 pub const fn q(&self) -> usize {
102 self.q
103 }
104}
105
106impl Indicator for VarianceRatio {
107 type Input = (f64, f64);
108 type Output = f64;
109
110 #[inline]
111 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
112 let (a, b) = input;
113 if !a.is_finite() || !b.is_finite() {
114 return None;
115 }
116 if self.window.len() == self.period {
117 self.window.pop_front();
118 }
119 self.window.push_back(a - b);
120 if self.window.len() < self.period {
121 return None;
122 }
123 self.scratch.clear();
124 self.scratch.extend(self.window.iter().copied());
125 let returns = &mut self.returns;
127 returns.clear();
128 returns.extend(self.scratch.windows(2).map(|w| w[1] - w[0]));
129 let m = returns.len() as f64;
130 let mean = returns.iter().sum::<f64>() / m;
131 let var_one = returns.iter().map(|r| (r - mean) * (r - mean)).sum::<f64>() / m;
132 if var_one <= 0.0 {
133 return Some(1.0);
135 }
136 let q_mean = self.q as f64 * mean;
138 let mut sum_sq = 0.0;
141 let mut count = 0.0;
142 for window in returns.windows(self.q) {
143 let deviation = window.iter().sum::<f64>() - q_mean;
144 sum_sq += deviation * deviation;
145 count += 1.0;
146 }
147 let var_q = sum_sq / count;
148 Some(var_q / (self.q as f64 * var_one))
149 }
150
151 fn reset(&mut self) {
152 self.window.clear();
153 self.scratch.clear();
154 self.returns.clear();
155 }
156
157 #[inline]
158 fn warmup_period(&self) -> usize {
159 self.period
160 }
161
162 #[inline]
163 fn is_ready(&self) -> bool {
164 self.window.len() == self.period
165 }
166
167 #[inline]
168 fn name(&self) -> &'static str {
169 "VarianceRatio"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::traits::BatchExt;
177 use approx::assert_relative_eq;
178
179 #[test]
180 fn rejects_bad_parameters() {
181 assert!(VarianceRatio::new(10, 1).is_err()); assert!(VarianceRatio::new(3, 2).is_err()); assert!(VarianceRatio::new(4, 2).is_ok());
184 }
185
186 #[test]
187 fn accessors_and_metadata() {
188 let vr = VarianceRatio::new(60, 4).unwrap();
189 assert_eq!(vr.period(), 60);
190 assert_eq!(vr.q(), 4);
191 assert_eq!(vr.warmup_period(), 60);
192 assert_eq!(vr.name(), "VarianceRatio");
193 assert!(!vr.is_ready());
194 }
195
196 #[test]
197 fn warmup_returns_none() {
198 let mut vr = VarianceRatio::new(4, 2).unwrap();
199 assert_eq!(vr.update((1.0, 0.0)), None);
200 assert_eq!(vr.update((2.0, 0.0)), None);
201 assert_eq!(vr.update((3.0, 0.0)), None);
202 assert!(vr.update((4.0, 0.0)).is_some());
203 assert!(vr.is_ready());
204 }
205
206 #[test]
207 fn alternating_changes_give_zero_ratio() {
208 let pairs = [(0.0, 0.0), (2.0, 0.0), (1.0, 0.0), (3.0, 0.0), (2.0, 0.0)];
211 let last = VarianceRatio::new(5, 2)
212 .unwrap()
213 .batch(&pairs)
214 .into_iter()
215 .flatten()
216 .last()
217 .unwrap();
218 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
219 }
220
221 #[test]
222 fn oscillating_spread_is_below_one() {
223 let pairs: Vec<(f64, f64)> = (0..200)
224 .map(|t| {
225 let b = 100.0 + f64::from(t);
226 (b + 2.0 * (f64::from(t) * 2.5).sin(), b)
227 })
228 .collect();
229 let last = VarianceRatio::new(60, 2)
230 .unwrap()
231 .batch(&pairs)
232 .into_iter()
233 .flatten()
234 .last()
235 .unwrap();
236 assert!(last < 1.0, "VR {last}");
237 }
238
239 #[test]
240 fn flat_spread_returns_one() {
241 let pairs: Vec<(f64, f64)> = (0..30)
242 .map(|t| (5.0 + f64::from(t), f64::from(t)))
243 .collect();
244 let last = VarianceRatio::new(10, 3)
245 .unwrap()
246 .batch(&pairs)
247 .into_iter()
248 .flatten()
249 .last()
250 .unwrap();
251 assert_eq!(last, 1.0);
252 }
253
254 #[test]
255 fn output_non_negative() {
256 let pairs: Vec<(f64, f64)> = (0..150)
257 .map(|t| {
258 let b = 50.0 + 0.3 * f64::from(t);
259 (b + (f64::from(t) * 0.5).sin() * 2.0, b)
260 })
261 .collect();
262 let mut vr = VarianceRatio::new(40, 4).unwrap();
263 for v in vr.batch(&pairs).into_iter().flatten() {
264 assert!(v >= 0.0, "VR {v}");
265 }
266 }
267
268 #[test]
269 fn reset_clears_state() {
270 let mut vr = VarianceRatio::new(6, 2).unwrap();
271 for t in 0..12 {
272 vr.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
273 }
274 assert!(vr.is_ready());
275 vr.reset();
276 assert!(!vr.is_ready());
277 assert_eq!(vr.update((1.0, 0.0)), None);
278 }
279
280 #[test]
281 fn batch_equals_streaming() {
282 let pairs: Vec<(f64, f64)> = (0..100)
283 .map(|t| {
284 let b = 30.0 + 0.7 * f64::from(t);
285 (b + (f64::from(t) * 0.4).sin() * 1.5, b)
286 })
287 .collect();
288 let batch = VarianceRatio::new(32, 3).unwrap().batch(&pairs);
289 let mut vr = VarianceRatio::new(32, 3).unwrap();
290 let streamed: Vec<_> = pairs.iter().map(|p| vr.update(*p)).collect();
291 assert_eq!(batch, streamed);
292 }
293
294 #[test]
295 fn non_finite_input_returns_none() {
296 let mut vr = VarianceRatio::new(4, 2).unwrap();
297 assert_eq!(vr.update((f64::NAN, 1.0)), None);
298 assert_eq!(vr.update((1.0, f64::INFINITY)), None);
299 assert_eq!(vr.update((1.0, 0.0)), None);
301 assert_eq!(vr.update((2.0, 0.0)), None);
302 assert_eq!(vr.update((3.0, 0.0)), None);
303 assert!(vr.update((4.0, 0.0)).is_some());
304 }
305}