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