Skip to main content

quantwave_core/indicators/
voss_predictor.rs

1use crate::indicators::bandpass::BandPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use crate::utils::RingBuffer as 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 {
75            name: "period",
76            default: "20",
77            description: "Center period of the BandPass filter",
78        },
79        ParamDef {
80            name: "predict",
81            default: "3",
82            description: "Number of bars of prediction",
83        },
84    ],
85    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/A%20PEEK%20INTO%20THE%20FUTURE.pdf",
86    formula_latex: r#"
87\[
88Filt = \text{BandPass}(Price, Period, 0.25)
89\]
90\[
91Order = 3 \cdot Predict
92\]
93\[
94SumC = \sum_{n=0}^{Order-1} \frac{n+1}{Order} Voss_{t-(Order-n)}
95\]
96\[
97Voss = \frac{3 + Order}{2} Filt - SumC
98\]
99"#,
100    gold_standard_file: "voss_predictor.json",
101    category: "Ehlers DSP",
102};
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::test_utils::{assert_indicator_parity_tuple, load_gold_standard_tuple};
108    use crate::traits::Next;
109    use proptest::prelude::*;
110
111    #[test]
112    fn test_voss_gold_standard() {
113        let case = load_gold_standard_tuple("voss_predictor");
114        let vp = VossPredictor::new(20, 3);
115        assert_indicator_parity_tuple(vp, &case.input, &case.expected);
116    }
117
118    #[test]
119    fn test_voss_basic() {
120        let mut vp = VossPredictor::default();
121        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
122        for input in inputs {
123            let (filt, voss) = vp.next(input);
124            assert!(!filt.is_nan());
125            assert!(!voss.is_nan());
126        }
127    }
128
129    proptest! {
130        #[test]
131        fn test_voss_parity(
132            inputs in prop::collection::vec(1.0..100.0, 60..120),
133        ) {
134            let period = 20;
135            let predict = 3;
136            let mut vp = VossPredictor::new(period, predict);
137            let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| vp.next(x)).collect();
138
139            // Batch implementation
140            let mut batch_results = Vec::with_capacity(inputs.len());
141            let mut bp = BandPass::new(period, 0.25);
142            let order = 3 * predict;
143            let mut v_hist = VecDeque::with_capacity(order + 1);
144
145            for &input in &inputs {
146                let filt = bp.next(input);
147                let mut sum_c = 0.0;
148                for count in 0..order {
149                    let idx = order - count;
150                    let val = if idx <= v_hist.len() {
151                        v_hist[idx - 1]
152                    } else {
153                        0.0
154                    };
155                    sum_c += ((count + 1) as f64 / order as f64) * val;
156                }
157
158                let voss = ((3.0 + order as f64) / 2.0) * filt - sum_c;
159                v_hist.push_front(voss);
160                if v_hist.len() > order {
161                    v_hist.pop_back();
162                }
163                batch_results.push((filt, voss));
164            }
165
166            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
167                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
168                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
169            }
170        }
171    }
172}