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