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 self.window.len() == self.period {
102 self.window.pop_front();
103 }
104 self.window.push_back(a - b);
105 if self.window.len() < self.period {
106 return None;
107 }
108 let spreads: Vec<f64> = self.window.iter().copied().collect();
109 let returns: Vec<f64> = spreads.windows(2).map(|w| w[1] - w[0]).collect();
111 let m = returns.len() as f64;
112 let mean = returns.iter().sum::<f64>() / m;
113 let var_one = returns.iter().map(|r| (r - mean) * (r - mean)).sum::<f64>() / m;
114 if var_one <= 0.0 {
115 return Some(1.0);
117 }
118 let q_mean = self.q as f64 * mean;
120 let long: Vec<f64> = returns.windows(self.q).map(|w| w.iter().sum()).collect();
121 let count = long.len() as f64;
122 let var_q = long
123 .iter()
124 .map(|y| (y - q_mean) * (y - q_mean))
125 .sum::<f64>()
126 / count;
127 Some(var_q / (self.q as f64 * var_one))
128 }
129
130 fn reset(&mut self) {
131 self.window.clear();
132 }
133
134 fn warmup_period(&self) -> usize {
135 self.period
136 }
137
138 fn is_ready(&self) -> bool {
139 self.window.len() == self.period
140 }
141
142 fn name(&self) -> &'static str {
143 "VarianceRatio"
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::traits::BatchExt;
151 use approx::assert_relative_eq;
152
153 #[test]
154 fn rejects_bad_parameters() {
155 assert!(VarianceRatio::new(10, 1).is_err()); assert!(VarianceRatio::new(3, 2).is_err()); assert!(VarianceRatio::new(4, 2).is_ok());
158 }
159
160 #[test]
161 fn accessors_and_metadata() {
162 let vr = VarianceRatio::new(60, 4).unwrap();
163 assert_eq!(vr.period(), 60);
164 assert_eq!(vr.q(), 4);
165 assert_eq!(vr.warmup_period(), 60);
166 assert_eq!(vr.name(), "VarianceRatio");
167 assert!(!vr.is_ready());
168 }
169
170 #[test]
171 fn warmup_returns_none() {
172 let mut vr = VarianceRatio::new(4, 2).unwrap();
173 assert_eq!(vr.update((1.0, 0.0)), None);
174 assert_eq!(vr.update((2.0, 0.0)), None);
175 assert_eq!(vr.update((3.0, 0.0)), None);
176 assert!(vr.update((4.0, 0.0)).is_some());
177 assert!(vr.is_ready());
178 }
179
180 #[test]
181 fn alternating_changes_give_zero_ratio() {
182 let pairs = [(0.0, 0.0), (2.0, 0.0), (1.0, 0.0), (3.0, 0.0), (2.0, 0.0)];
185 let last = VarianceRatio::new(5, 2)
186 .unwrap()
187 .batch(&pairs)
188 .into_iter()
189 .flatten()
190 .last()
191 .unwrap();
192 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
193 }
194
195 #[test]
196 fn oscillating_spread_is_below_one() {
197 let pairs: Vec<(f64, f64)> = (0..200)
198 .map(|t| {
199 let b = 100.0 + f64::from(t);
200 (b + 2.0 * (f64::from(t) * 2.5).sin(), b)
201 })
202 .collect();
203 let last = VarianceRatio::new(60, 2)
204 .unwrap()
205 .batch(&pairs)
206 .into_iter()
207 .flatten()
208 .last()
209 .unwrap();
210 assert!(last < 1.0, "VR {last}");
211 }
212
213 #[test]
214 fn flat_spread_returns_one() {
215 let pairs: Vec<(f64, f64)> = (0..30)
216 .map(|t| (5.0 + f64::from(t), f64::from(t)))
217 .collect();
218 let last = VarianceRatio::new(10, 3)
219 .unwrap()
220 .batch(&pairs)
221 .into_iter()
222 .flatten()
223 .last()
224 .unwrap();
225 assert_eq!(last, 1.0);
226 }
227
228 #[test]
229 fn output_non_negative() {
230 let pairs: Vec<(f64, f64)> = (0..150)
231 .map(|t| {
232 let b = 50.0 + 0.3 * f64::from(t);
233 (b + (f64::from(t) * 0.5).sin() * 2.0, b)
234 })
235 .collect();
236 let mut vr = VarianceRatio::new(40, 4).unwrap();
237 for v in vr.batch(&pairs).into_iter().flatten() {
238 assert!(v >= 0.0, "VR {v}");
239 }
240 }
241
242 #[test]
243 fn reset_clears_state() {
244 let mut vr = VarianceRatio::new(6, 2).unwrap();
245 for t in 0..12 {
246 vr.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
247 }
248 assert!(vr.is_ready());
249 vr.reset();
250 assert!(!vr.is_ready());
251 assert_eq!(vr.update((1.0, 0.0)), None);
252 }
253
254 #[test]
255 fn batch_equals_streaming() {
256 let pairs: Vec<(f64, f64)> = (0..100)
257 .map(|t| {
258 let b = 30.0 + 0.7 * f64::from(t);
259 (b + (f64::from(t) * 0.4).sin() * 1.5, b)
260 })
261 .collect();
262 let batch = VarianceRatio::new(32, 3).unwrap().batch(&pairs);
263 let mut vr = VarianceRatio::new(32, 3).unwrap();
264 let streamed: Vec<_> = pairs.iter().map(|p| vr.update(*p)).collect();
265 assert_eq!(batch, streamed);
266 }
267}