Skip to main content

quantwave_core/indicators/
griffiths_dominant_cycle.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 Dominant Cycle
9///
10/// Based on John Ehlers' "Linear Predictive Filters And Instantaneous Frequency" (TASC January 2025).
11/// Uses the Griffiths spectral estimation to find the dominant cycle in the data.
12#[derive(Debug, Clone)]
13pub struct GriffithsDominantCycle {
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    prev_cycle: f64,
24}
25
26impl GriffithsDominantCycle {
27    pub fn new(lower_bound: usize, upper_bound: usize, length: usize) -> Self {
28        Self {
29            lb: lower_bound,
30            ub: upper_bound,
31            length,
32            mu: 1.0 / (length as f64),
33            hp: HighPass::new(upper_bound),
34            ss: SuperSmoother::new(lower_bound),
35            peak: 0.1,
36            signal_window: VecDeque::with_capacity(length + 1),
37            coef: vec![0.0; length + 1],
38            prev_cycle: (lower_bound + upper_bound) as f64 / 2.0,
39        }
40    }
41}
42
43impl Default for GriffithsDominantCycle {
44    fn default() -> Self {
45        Self::new(18, 40, 40)
46    }
47}
48
49impl Next<f64> for GriffithsDominantCycle {
50    type Output = f64;
51
52    fn next(&mut self, input: f64) -> Self::Output {
53        let hp_val = self.hp.next(input);
54        let lp_val = self.ss.next(hp_val);
55
56        self.peak *= 0.991;
57        if lp_val.abs() > self.peak {
58            self.peak = lp_val.abs();
59        }
60
61        let signal = if self.peak != 0.0 {
62            lp_val / self.peak
63        } else {
64            0.0
65        };
66
67        self.signal_window.push_front(signal);
68        if self.signal_window.len() > self.length {
69            self.signal_window.pop_back();
70        }
71
72        if self.signal_window.len() < self.length {
73            return self.prev_cycle;
74        }
75
76        let mut xx = vec![0.0; self.length + 1];
77        for i in 1..=self.length {
78            xx[i] = 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        // Spectral scan
91        let mut max_pwr = 0.0;
92        let mut cycle = self.prev_cycle;
93
94        for period_idx in self.lb..=self.ub {
95            let period = period_idx as f64;
96            let mut real = 0.0;
97            let mut imag = 0.0;
98
99            for count in 1..=self.length {
100                let angle = 2.0 * PI * (count as f64) / period;
101                real += self.coef[count] * angle.cos();
102                imag += self.coef[count] * angle.sin();
103            }
104
105            let denom = (1.0 - real).powi(2) + imag.powi(2);
106            let pwr = 0.1 / denom;
107
108            if pwr > max_pwr {
109                max_pwr = pwr;
110                cycle = period;
111            }
112        }
113
114        // Slew rate limiter
115        if cycle > self.prev_cycle + 2.0 {
116            cycle = self.prev_cycle + 2.0;
117        } else if cycle < self.prev_cycle - 2.0 {
118            cycle = self.prev_cycle - 2.0;
119        }
120
121        self.prev_cycle = cycle;
122        cycle
123    }
124}
125
126pub const GRIFFITHS_DOMINANT_CYCLE_METADATA: IndicatorMetadata = IndicatorMetadata {
127    name: "GriffithsDominantCycle",
128    description: "Dominant cycle estimation using Griffiths adaptive spectral analysis.",
129    params: &[
130        ParamDef {
131            name: "lower_bound",
132            default: "18",
133            description: "Lower period bound",
134        },
135        ParamDef {
136            name: "upper_bound",
137            default: "40",
138            description: "Upper period bound",
139        },
140        ParamDef {
141            name: "length",
142            default: "40",
143            description: "LMS filter length",
144        },
145    ],
146    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202025.html",
147    formula_latex: r#"
148\[
149Pwr(Period) = \frac{0.1}{(1-Real)^2 + Imag^2}
150\]
151\[
152Real = \sum coef_i \cos(2\pi i / Period)
153\]
154\[
155Imag = \sum coef_i \sin(2\pi i / Period)
156\]
157"#,
158    gold_standard_file: "griffiths_dominant_cycle.json",
159    category: "Ehlers DSP",
160};
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::traits::Next;
166    use proptest::prelude::*;
167
168    #[test]
169    fn test_griffiths_dc_basic() {
170        let mut gdc = GriffithsDominantCycle::new(18, 40, 40);
171        for i in 0..200 {
172            // Sine wave with period 30
173            let val = gdc.next((2.0 * PI * i as f64 / 30.0).sin());
174            if i > 150 {
175                // Should converge towards 30
176                assert!(val > 25.0 && val < 35.0);
177            }
178        }
179    }
180
181    proptest! {
182        #[test]
183        fn test_griffiths_dc_parity(
184            inputs in prop::collection::vec(1.0..100.0, 100..200),
185        ) {
186            let lb = 18;
187            let ub = 40;
188            let length = 40;
189            let mut gdc = GriffithsDominantCycle::new(lb, ub, length);
190            let streaming_results: Vec<f64> = inputs.iter().map(|&x| gdc.next(x)).collect();
191
192            // Batch implementation
193            let mut batch_results = Vec::with_capacity(inputs.len());
194            let mut hp = HighPass::new(ub);
195            let mut ss = SuperSmoother::new(lb);
196            let lp_vals: Vec<f64> = inputs.iter().map(|&x| ss.next(hp.next(x))).collect();
197
198            let mut peak = 0.1;
199            let mut signals = Vec::new();
200            let mut coef = vec![0.0; length + 1];
201            let mu = 1.0 / length as f64;
202            let mut prev_cycle = (lb + ub) as f64 / 2.0;
203
204            for (i, &lp_val) in lp_vals.iter().enumerate() {
205                peak *= 0.991;
206                if lp_val.abs() > peak {
207                    peak = lp_val.abs();
208                }
209                let signal = if peak != 0.0 { lp_val / peak } else { 0.0 };
210                signals.push(signal);
211
212                if signals.len() < length {
213                    batch_results.push(prev_cycle);
214                    continue;
215                }
216
217                let mut xx = vec![0.0; length + 1];
218                for j in 1..=length {
219                    xx[j] = signals[i - (length - j)];
220                }
221
222                let mut x_bar = 0.0;
223                for count in 1..=length {
224                    x_bar += xx[length - count] * coef[count];
225                }
226
227                for count in 1..=length {
228                    coef[count] += mu * (xx[length] - x_bar) * xx[length - count];
229                }
230
231                let mut max_pwr = 0.0;
232                let mut cycle = prev_cycle;
233
234                for period_idx in lb..=ub {
235                    let period = period_idx as f64;
236                    let mut real = 0.0;
237                    let mut imag = 0.0;
238                    for count in 1..=length {
239                        let angle = 2.0 * PI * (count as f64) / period;
240                        real += coef[count] * angle.cos();
241                        imag += coef[count] * angle.sin();
242                    }
243                    let denom = (1.0 - real).powi(2) + imag.powi(2);
244                    let pwr = 0.1 / denom;
245                    if pwr > max_pwr {
246                        max_pwr = pwr;
247                        cycle = period;
248                    }
249                }
250
251                if cycle > prev_cycle + 2.0 {
252                    cycle = prev_cycle + 2.0;
253                } else if cycle < prev_cycle - 2.0 {
254                    cycle = prev_cycle - 2.0;
255                }
256                
257                prev_cycle = cycle;
258                batch_results.push(cycle);
259            }
260
261            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
262                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
263            }
264        }
265    }
266}