Skip to main content

quantwave_core/indicators/
voss_predictor.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::bandpass::BandPass;
4use std::collections::VecDeque;
5
6/// Voss Predictive Filter
7///
8/// Based on John Ehlers' "A Peek Into The Future".
9/// Uses a two-pole bandpass filter followed by a Voss predictor to achieve
10/// negative group delay for band-limited signals.
11#[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), // Bandwidth default 0.25 as per paper
23            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); // (Filt, Voss)
37
38    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                // voss_history[0] is Voss[1] (value 1 bar ago)
46                // voss_history[idx - 1] is Voss[idx]
47                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            // Batch implementation
132            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}