quantwave_core/indicators/
griffiths_dominant_cycle.rs1use 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#[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 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 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 let val = gdc.next((2.0 * PI * i as f64 / 30.0).sin());
174 if i > 150 {
175 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 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}