Skip to main content

quantwave_core/indicators/
griffiths_spectrum.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;
6use std::f64::consts::PI;
7
8/// Griffiths Spectrum
9///
10/// Based on John Ehlers' "Linear Predictive Filters And Instantaneous Frequency" (TASC January 2025).
11/// It computes the normalized power spectrum using Griffiths adaptive filter coefficients.
12#[derive(Debug, Clone)]
13pub struct GriffithsSpectrum {
14    lb: usize,
15    ub: usize,
16    length: usize,
17    mu: f64,
18    hp: HighPass,
19    ss: SuperSmoother,
20    peak: f64,
21    signal_window: VecDeque<f64>,
22    coef: Vec<f64>,
23}
24
25impl GriffithsSpectrum {
26    pub fn new(lower_bound: usize, upper_bound: usize, length: usize) -> Self {
27        Self {
28            lb: lower_bound,
29            ub: upper_bound,
30            length,
31            mu: 1.0 / (length as f64),
32            hp: HighPass::new(upper_bound),
33            ss: SuperSmoother::new(lower_bound),
34            peak: 0.1,
35            signal_window: VecDeque::with_capacity(length + 1),
36            coef: vec![0.0; length + 1],
37        }
38    }
39}
40
41impl Default for GriffithsSpectrum {
42    fn default() -> Self {
43        Self::new(18, 40, 40)
44    }
45}
46
47impl Next<f64> for GriffithsSpectrum {
48    type Output = Vec<f64>; // Power for each period from lb to ub
49
50    fn next(&mut self, input: f64) -> Self::Output {
51        let hp_val = self.hp.next(input);
52        let lp_val = self.ss.next(hp_val);
53
54        self.peak *= 0.991;
55        if lp_val.abs() > self.peak {
56            self.peak = lp_val.abs();
57        }
58
59        let signal = if self.peak != 0.0 {
60            lp_val / self.peak
61        } else {
62            0.0
63        };
64
65        self.signal_window.push_front(signal);
66        if self.signal_window.len() > self.length {
67            self.signal_window.pop_back();
68        }
69
70        let mut results = vec![0.0; self.ub - self.lb + 1];
71
72        if self.signal_window.len() < self.length {
73            return results;
74        }
75
76        let mut xx = vec![0.0; self.length + 1];
77        for (i, val) in xx.iter_mut().enumerate().skip(1).take(self.length) {
78            *val = self.signal_window[self.length - i];
79        }
80
81        let mut x_bar = 0.0;
82        for count in 1..=self.length {
83            x_bar += xx[self.length - count] * self.coef[count];
84        }
85
86        for count in 1..=self.length {
87            self.coef[count] += self.mu * (xx[self.length] - x_bar) * xx[self.length - count];
88        }
89
90        let mut max_pwr = 0.0;
91        let mut powers = Vec::with_capacity(self.ub - self.lb + 1);
92
93        for period_idx in self.lb..=self.ub {
94            let period = period_idx as f64;
95            let mut real = 0.0;
96            let mut imag = 0.0;
97
98            for count in 1..=self.length {
99                let angle = 2.0 * PI * (count as f64) / period;
100                real += self.coef[count] * angle.cos();
101                imag += self.coef[count] * angle.sin();
102            }
103
104            let denom = (1.0 - real).powi(2) + imag.powi(2);
105            let pwr = 0.1 / denom;
106            
107            if pwr > max_pwr {
108                max_pwr = pwr;
109            }
110            powers.push(pwr);
111        }
112
113        if max_pwr != 0.0 {
114            for (i, pwr) in powers.into_iter().enumerate() {
115                results[i] = pwr / max_pwr;
116            }
117        }
118
119        results
120    }
121}
122
123pub const GRIFFITHS_SPECTRUM_METADATA: IndicatorMetadata = IndicatorMetadata {
124    name: "GriffithsSpectrum",
125    description: "Normalized power spectrum estimation using Griffiths adaptive filters.",
126    usage: "Use to generate a high-resolution periodogram for cycle analysis. Best visualized as a heatmap to identify and track multiple market cycles simultaneously.",
127    keywords: &["spectrum", "cycle", "ehlers", "dsp", "periodogram"],
128    ehlers_summary: "The Griffiths Spectrum is an adaptive spectral estimation method that provides higher resolution than a standard DFT for short data segments. It fits an all-pole model to the signal using an LMS algorithm, allowing for instantaneous frequency measurement without the windowing artifacts of FFT-based methods.",
129    params: &[
130        ParamDef { name: "lower_bound", default: "18", description: "Lower period bound" },
131        ParamDef { name: "upper_bound", default: "40", description: "Upper period bound" },
132        ParamDef { name: "length", default: "40", description: "LMS filter length" },
133    ],
134    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202025.html",
135    formula_latex: r#"
136\[
137Pwr(P) = \frac{0.1}{(1 - \sum coef_i \cos(2\pi i/P))^2 + (\sum coef_i \sin(2\pi i/P))^2}
138\]
139\[
140Pwr_{norm}(P) = \frac{Pwr(P)}{\max(Pwr)}
141\]
142"#,
143    gold_standard_file: "griffiths_spectrum.json",
144    category: "Ehlers DSP",
145};
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::traits::Next;
151    // use crate::test_utils;
152    // use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
153    use proptest::prelude::*;
154
155    /*
156    #[test]
157    fn test_griffiths_spectrum_gold_standard() {
158        let case = load_gold_standard_vec("griffiths_spectrum");
159        let gs = GriffithsSpectrum::new(18, 40, 40);
160        assert_indicator_parity_vec(gs, &case.input, &case.expected);
161    }
162    */
163    // TODO: Restore test once griffiths_spectrum.json is recovered.
164
165    #[test]
166    fn test_griffiths_spectrum_basic() {
167        let mut gs = GriffithsSpectrum::new(18, 40, 40);
168        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
169        for input in inputs {
170            let res = gs.next(input);
171            assert_eq!(res.len(), 40 - 18 + 1);
172        }
173    }
174
175    proptest! {
176        #[test]
177        fn test_griffiths_spectrum_parity(
178            inputs in prop::collection::vec(1.0..100.0, 100..200),
179        ) {
180            let lb = 18;
181            let ub = 40;
182            let length = 40;
183            let mut gs = GriffithsSpectrum::new(lb, ub, length);
184            let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| gs.next(x)).collect();
185
186            // Batch implementation
187            let mut batch_results = Vec::with_capacity(inputs.len());
188            let mut hp = HighPass::new(ub);
189            let mut ss = SuperSmoother::new(lb);
190            let lp_vals: Vec<f64> = inputs.iter().map(|&x| ss.next(hp.next(x))).collect();
191
192            let mut peak = 0.1;
193            let mut signals = Vec::new();
194            let mut coef = vec![0.0; length + 1];
195            let mu = 1.0 / length as f64;
196
197            for (i, &lp_val) in lp_vals.iter().enumerate() {
198                peak *= 0.991;
199                if lp_val.abs() > peak {
200                    peak = lp_val.abs();
201                }
202                let signal = if peak != 0.0 { lp_val / peak } else { 0.0 };
203                signals.push(signal);
204
205                if signals.len() < length {
206                    batch_results.push(vec![0.0; ub - lb + 1]);
207                    continue;
208                }
209
210                let mut xx = vec![0.0; length + 1];
211                for j in 1..=length {
212                    xx[j] = signals[i - (length - j)];
213                }
214
215                let mut x_bar = 0.0;
216                for count in 1..=length {
217                    x_bar += xx[length - count] * coef[count];
218                }
219
220                for count in 1..=length {
221                    coef[count] += mu * (xx[length] - x_bar) * xx[length - count];
222                }
223
224                let mut powers = Vec::new();
225                let mut max_pwr = 0.0;
226                for period_idx in lb..=ub {
227                    let period = period_idx as f64;
228                    let mut real = 0.0;
229                    let mut imag = 0.0;
230                    for count in 1..=length {
231                        let angle = 2.0 * PI * (count as f64) / period;
232                        real += coef[count] * angle.cos();
233                        imag += coef[count] * angle.sin();
234                    }
235                    let denom = (1.0 - real).powi(2) + imag.powi(2);
236                    let pwr = 0.1 / denom;
237                    if pwr > max_pwr { max_pwr = pwr; }
238                    powers.push(pwr);
239                }
240                
241                let norm_powers = if max_pwr != 0.0 {
242                    powers.into_iter().map(|p| p / max_pwr).collect()
243                } else {
244                    vec![0.0; ub - lb + 1]
245                };
246                batch_results.push(norm_powers);
247            }
248
249            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
250                for (sv, bv) in s.iter().zip(b.iter()) {
251                    approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
252                }
253            }
254        }
255    }
256}