quantwave_core/indicators/
voss_predictor.rs1use crate::indicators::bandpass::BandPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use crate::utils::RingBuffer as 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 {
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 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}