Skip to main content

quantwave_core/indicators/
griffiths_predictor.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::high_pass::HighPass;
4use crate::indicators::super_smoother::SuperSmoother;
5use std::collections::VecDeque;
6
7/// Griffiths Predictor
8///
9/// Based on John Ehlers' "Linear Predictive Filters And Instantaneous Frequency" (TASC January 2025).
10/// Uses an adaptive LMS (Griffiths) algorithm to predict future signal values.
11#[derive(Debug, Clone)]
12pub struct GriffithsPredictor {
13    length: usize,
14    bars_fwd: usize,
15    mu: f64,
16    hp: HighPass,
17    ss: SuperSmoother,
18    peak: f64,
19    signal_window: VecDeque<f64>,
20    coef: Vec<f64>,
21}
22
23impl GriffithsPredictor {
24    pub fn new(lower_bound: usize, upper_bound: usize, length: usize, bars_fwd: usize) -> Self {
25        Self {
26            length,
27            bars_fwd,
28            mu: 1.0 / (length as f64),
29            hp: HighPass::new(upper_bound),
30            ss: SuperSmoother::new(lower_bound),
31            peak: 0.1,
32            signal_window: VecDeque::with_capacity(length + 1),
33            coef: vec![0.0; length + 1], // 1-indexed logic compatibility
34        }
35    }
36}
37
38impl Default for GriffithsPredictor {
39    fn default() -> Self {
40        Self::new(18, 40, 18, 2)
41    }
42}
43
44impl Next<f64> for GriffithsPredictor {
45    type Output = f64;
46
47    fn next(&mut self, input: f64) -> Self::Output {
48        let hp_val = self.hp.next(input);
49        let lp_val = self.ss.next(hp_val);
50
51        // Peak detection
52        self.peak *= 0.991;
53        if lp_val.abs() > self.peak {
54            self.peak = lp_val.abs();
55        }
56
57        let signal = if self.peak != 0.0 {
58            lp_val / self.peak
59        } else {
60            0.0
61        };
62
63        self.signal_window.push_front(signal);
64        if self.signal_window.len() > self.length {
65            self.signal_window.pop_back();
66        }
67
68        if self.signal_window.len() < self.length {
69            return 0.0;
70        }
71
72        // Current signal is at index 0 (latest)
73        // Previous signals are at indices 1..Length-1
74        // Ehlers' XX[Length] is current signal.
75        // XX[Length - count] is previous signals.
76        // XX[Length - 1] = window[1]
77        // XX[Length - length] = window[length]? Wait.
78        
79        // Let's use Ehlers' indexing directly by copying to a temp vector
80        let mut xx = vec![0.0; self.length + 1];
81        for (i, val) in xx.iter_mut().enumerate().skip(1).take(self.length) {
82            *val = self.signal_window[self.length - i];
83        }
84
85        let mut x_bar = 0.0;
86        for count in 1..=self.length {
87            x_bar += xx[self.length - count] * self.coef[count];
88        }
89
90        for count in 1..=self.length {
91            self.coef[count] += self.mu * (xx[self.length] - x_bar) * xx[self.length - count];
92        }
93
94        // Prediction
95        let mut x_pred = 0.0;
96        let mut xx_temp = xx.clone();
97        for _advance in 1..=self.bars_fwd {
98            x_pred = 0.0;
99            for count in 1..=self.length {
100                x_pred += xx_temp[self.length + 1 - count] * self.coef[count];
101            }
102            
103            // Shift
104            for count in 1..self.length {
105                xx_temp[count] = xx_temp[count + 1];
106            }
107            xx_temp[self.length] = x_pred;
108        }
109
110        x_pred
111    }
112}
113
114pub const GRIFFITHS_PREDICTOR_METADATA: IndicatorMetadata = IndicatorMetadata {
115    name: "GriffithsPredictor",
116    description: "Adaptive LMS linear predictive filter for signal forecasting.",
117    usage: "Use for short-horizon price prediction by projecting the dominant market cycle one or two bars forward. Works best in oscillating markets; disable in strong trends.",
118    keywords: &["prediction", "cycle", "ehlers", "dsp"],
119    ehlers_summary: "The Griffiths Predictor uses autoregressive coefficients from the Griffiths cycle measurement to extrapolate the current dominant cycle one bar ahead. By fitting an AR model to cycle-filtered price, it generates a one-step-ahead forecast useful for anticipatory entries at predicted cycle turns.",
120    params: &[
121        ParamDef {
122            name: "lower_bound",
123            default: "18",
124            description: "Lower frequency bound (SS length)",
125        },
126        ParamDef {
127            name: "upper_bound",
128            default: "40",
129            description: "Upper frequency bound (HP length)",
130        },
131        ParamDef {
132            name: "length",
133            default: "18",
134            description: "LMS filter length",
135        },
136        ParamDef {
137            name: "bars_fwd",
138            default: "2",
139            description: "Number of bars to predict forward",
140        },
141    ],
142    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202025.html",
143    formula_latex: r#"
144\[
145\mu = 1/L
146\]
147\[
148\bar{x} = \sum_{i=1}^L xx_{L-i} \cdot coef_i
149\]
150\[
151coef_i = coef_i + \mu(xx_L - \bar{x})xx_{L-i}
152\]
153"#,
154    gold_standard_file: "griffiths_predictor.json",
155    category: "Ehlers DSP",
156};
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::traits::Next;
162    use proptest::prelude::*;
163
164    #[test]
165    fn test_griffiths_predictor_basic() {
166        let mut gp = GriffithsPredictor::new(18, 40, 18, 2);
167        for i in 0..100 {
168            let val = gp.next(100.0 + (i as f64 * 0.1).sin());
169            assert!(!val.is_nan());
170        }
171    }
172
173    proptest! {
174        #[test]
175        fn test_griffiths_predictor_parity(
176            inputs in prop::collection::vec(1.0..100.0, 100..200),
177        ) {
178            let lb = 18;
179            let ub = 40;
180            let length = 18;
181            let bars_fwd = 2;
182            let mut gp = GriffithsPredictor::new(lb, ub, length, bars_fwd);
183            let streaming_results: Vec<f64> = inputs.iter().map(|&x| gp.next(x)).collect();
184
185            // Batch implementation
186            let mut batch_results = Vec::with_capacity(inputs.len());
187            let mut hp = HighPass::new(ub);
188            let mut ss = SuperSmoother::new(lb);
189            let lp_vals: Vec<f64> = inputs.iter().map(|&x| ss.next(hp.next(x))).collect();
190
191            let mut peak = 0.1;
192            let mut signals = Vec::new();
193            let mut coef = vec![0.0; length + 1];
194            let mu = 1.0 / length as f64;
195
196            for (i, &lp_val) in lp_vals.iter().enumerate() {
197                peak *= 0.991;
198                if lp_val.abs() > peak {
199                    peak = lp_val.abs();
200                }
201                let signal = if peak != 0.0 { lp_val / peak } else { 0.0 };
202                signals.push(signal);
203
204                if signals.len() < length {
205                    batch_results.push(0.0);
206                    continue;
207                }
208
209                let mut xx = vec![0.0; length + 1];
210                for j in 1..=length {
211                    xx[j] = signals[i - (length - j)];
212                }
213
214                let mut x_bar = 0.0;
215                for count in 1..=length {
216                    x_bar += xx[length - count] * coef[count];
217                }
218
219                for count in 1..=length {
220                    coef[count] += mu * (xx[length] - x_bar) * xx[length - count];
221                }
222
223                let mut x_pred = 0.0;
224                let mut xx_temp = xx.clone();
225                for _advance in 1..=bars_fwd {
226                    x_pred = 0.0;
227                    for count in 1..=length {
228                        x_pred += xx_temp[length + 1 - count] * coef[count];
229                    }
230                    for count in 1..length {
231                        xx_temp[count] = xx_temp[count + 1];
232                    }
233                    xx_temp[length] = x_pred;
234                }
235                batch_results.push(x_pred);
236            }
237
238            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
239                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
240            }
241        }
242    }
243}