quantwave_core/indicators/
voss_predictor.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::bandpass::BandPass;
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone)]
12pub struct VossPredictor {
13 bandpass: BandPass,
14 order: usize,
15 voss_history: VecDeque<f64>,
16}
17
18impl VossPredictor {
19 pub fn new(period: usize, predict: usize) -> Self {
20 let order = 3 * predict;
21 Self {
22 bandpass: BandPass::new(period, 0.25), order,
24 voss_history: VecDeque::with_capacity(order + 1),
25 }
26 }
27}
28
29impl Default for VossPredictor {
30 fn default() -> Self {
31 Self::new(20, 3)
32 }
33}
34
35impl Next<f64> for VossPredictor {
36 type Output = (f64, f64); fn next(&mut self, input: f64) -> Self::Output {
39 let filt = self.bandpass.next(input);
40
41 let mut sum_c = 0.0;
42 if self.order > 0 {
43 for count in 0..self.order {
44 let idx = self.order - count;
45 let val = if idx <= self.voss_history.len() {
48 self.voss_history[idx - 1]
49 } else {
50 0.0
51 };
52 sum_c += ((count + 1) as f64 / self.order as f64) * val;
53 }
54 }
55
56 let voss = ((3.0 + self.order as f64) / 2.0) * filt - sum_c;
57
58 self.voss_history.push_front(voss);
59 if self.voss_history.len() > self.order {
60 self.voss_history.pop_back();
61 }
62
63 (filt, voss)
64 }
65}
66
67pub const VOSS_PREDICTOR_METADATA: IndicatorMetadata = IndicatorMetadata {
68 name: "VossPredictor",
69 description: "A predictive filter with negative group delay for band-limited signals.",
70 usage: "Use for multi-bar price prediction based on a bandpass-filtered dominant cycle. More accurate than simple linear extrapolation due to its IIR filter pole placement.",
71 keywords: &["prediction", "cycle", "ehlers", "dsp", "filter"],
72 ehlers_summary: "The Voss Predictor is a predictive filter developed by J.F. Voss and adapted by Ehlers in Cycle Analytics for Traders. Its IIR bandpass design inherently extrapolates the filtered signal several bars into the future by virtue of pole placement inside the unit circle, enabling lookahead without buffer access.",
73 params: &[
74 ParamDef { name: "period", default: "20", description: "Center period of the BandPass filter" },
75 ParamDef { name: "predict", default: "3", description: "Number of bars of prediction" },
76 ],
77 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/A%20PEEK%20INTO%20THE%20FUTURE.pdf",
78 formula_latex: r#"
79\[
80Filt = \text{BandPass}(Price, Period, 0.25)
81\]
82\[
83Order = 3 \cdot Predict
84\]
85\[
86SumC = \sum_{n=0}^{Order-1} \frac{n+1}{Order} Voss_{t-(Order-n)}
87\]
88\[
89Voss = \frac{3 + Order}{2} Filt - SumC
90\]
91"#,
92 gold_standard_file: "voss_predictor.json",
93 category: "Ehlers DSP",
94};
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::traits::Next;
100 use crate::test_utils::{load_gold_standard_tuple, assert_indicator_parity_tuple};
101 use proptest::prelude::*;
102
103 #[test]
104 fn test_voss_gold_standard() {
105 let case = load_gold_standard_tuple("voss_predictor");
106 let vp = VossPredictor::new(20, 3);
107 assert_indicator_parity_tuple(vp, &case.input, &case.expected);
108 }
109
110 #[test]
111 fn test_voss_basic() {
112 let mut vp = VossPredictor::default();
113 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
114 for input in inputs {
115 let (filt, voss) = vp.next(input);
116 assert!(!filt.is_nan());
117 assert!(!voss.is_nan());
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_voss_parity(
124 inputs in prop::collection::vec(1.0..100.0, 60..120),
125 ) {
126 let period = 20;
127 let predict = 3;
128 let mut vp = VossPredictor::new(period, predict);
129 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| vp.next(x)).collect();
130
131 let mut batch_results = Vec::with_capacity(inputs.len());
133 let mut bp = BandPass::new(period, 0.25);
134 let order = 3 * predict;
135 let mut v_hist = VecDeque::new();
136
137 for &input in &inputs {
138 let filt = bp.next(input);
139 let mut sum_c = 0.0;
140 for count in 0..order {
141 let idx = order - count;
142 let val = if idx <= v_hist.len() {
143 v_hist[idx - 1]
144 } else {
145 0.0
146 };
147 sum_c += ((count + 1) as f64 / order as f64) * val;
148 }
149
150 let voss = ((3.0 + order as f64) / 2.0) * filt - sum_c;
151 v_hist.push_front(voss);
152 if v_hist.len() > order {
153 v_hist.pop_back();
154 }
155 batch_results.push((filt, voss));
156 }
157
158 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
160 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
161 }
162 }
163 }
164}