Skip to main content

quantwave_core/indicators/
fourier_series.rs

1use crate::indicators::bandpass::BandPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::indicators::smoothing::SMA;
4use crate::traits::Next;
5use std::f64::consts::PI;
6
7/// Fourier Series Model
8///
9/// Based on John Ehlers' "Fourier Series Model of the Market".
10/// Synthesizes a smoothed market waveform by combining the fundamental,
11/// second, and third harmonics of a base cycle, weighted by their
12/// relative power.
13#[derive(Debug, Clone)]
14pub struct FourierSeriesModel {
15    fundamental: usize,
16    bp1: BandPass,
17    bp2: BandPass,
18    bp3: BandPass,
19    bp1_prev: f64,
20    bp2_prev: f64,
21    bp3_prev: f64,
22    p1_sma: SMA,
23    p2_sma: SMA,
24    p3_sma: SMA,
25    count: usize,
26}
27
28impl FourierSeriesModel {
29    pub fn new(fundamental: usize) -> Self {
30        Self {
31            fundamental,
32            bp1: BandPass::new(fundamental, 0.1),
33            bp2: BandPass::new(fundamental / 2, 0.1),
34            bp3: BandPass::new(fundamental / 3, 0.1),
35            bp1_prev: 0.0,
36            bp2_prev: 0.0,
37            bp3_prev: 0.0,
38            p1_sma: SMA::new(fundamental),
39            p2_sma: SMA::new(fundamental),
40            p3_sma: SMA::new(fundamental),
41            count: 0,
42        }
43    }
44}
45
46impl Default for FourierSeriesModel {
47    fn default() -> Self {
48        Self::new(20)
49    }
50}
51
52impl Next<f64> for FourierSeriesModel {
53    type Output = f64;
54
55    fn next(&mut self, input: f64) -> Self::Output {
56        self.count += 1;
57
58        let bp1 = self.bp1.next(input);
59        let bp2 = self.bp2.next(input);
60        let bp3 = self.bp3.next(input);
61
62        // Quadrature components (approximate derivatives)
63        // Q = (Fundamental / 6.28) * (BP - BP[1])
64        let q_scale = self.fundamental as f64 / (2.0 * PI);
65        let q1 = q_scale * (bp1 - self.bp1_prev);
66        let q2 = q_scale * (bp2 - self.bp2_prev);
67        let q3 = q_scale * (bp3 - self.bp3_prev);
68
69        // Power components (summed over fundamental period)
70        let p1 = self.p1_sma.next(bp1 * bp1 + q1 * q1) * self.fundamental as f64;
71        let p2 = self.p2_sma.next(bp2 * bp2 + q2 * q2) * self.fundamental as f64;
72        let p3 = self.p3_sma.next(bp3 * bp3 + q3 * q3) * self.fundamental as f64;
73
74        // Shift history
75        self.bp1_prev = bp1;
76        self.bp2_prev = bp2;
77        self.bp3_prev = bp3;
78
79        // Synthesized wave
80        // Wave = BP1 + sqrt(P2/P1)*BP2 + sqrt(P3/P1)*BP3
81        let mut wave = bp1;
82        if p1 > 0.0 {
83            wave += (p2 / p1).sqrt() * bp2;
84            wave += (p3 / p1).sqrt() * bp3;
85        }
86
87        wave
88    }
89}
90
91pub const FOURIER_SERIES_MODEL_METADATA: IndicatorMetadata = IndicatorMetadata {
92    name: "FourierSeriesModel",
93    description: "Synthesized market model using fundamental and harmonic frequency components.",
94    usage: "Use to model price as a sum of sine wave harmonics for short-term prediction. Most effective in clearly cyclical markets; combine with a cycle mode detector to disable it in trends.",
95    keywords: &["cycle", "spectral", "ehlers", "prediction", "fourier"],
96    ehlers_summary: "The Fourier Series Model fits harmonically related sine waves to recent price history using least-squares coefficients. Ehlers shows that projecting this model one bar forward gives a price forecast useful for anticipatory entry timing at predicted cycle turns.",
97    params: &[ParamDef {
98        name: "fundamental",
99        default: "20",
100        description: "Fundamental cycle period",
101    }],
102    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/FOURIER%20SERIES%20MODEL%20OF%20THE%20MARKET.pdf",
103    formula_latex: r#"
104\[
105BP_k = \text{BandPass}(Price, Fundamental/k)
106\]
107\[
108Q_k = \frac{Fundamental}{2\pi} (BP_{k} - BP_{k,t-1})
109\]
110\[
111P_k = \sum_{n=0}^{F-1} (BP_{k,t-n}^2 + Q_{k,t-n}^2)
112\]
113\[
114Wave = BP_1 + \sqrt{P_2/P_1}BP_2 + \sqrt{P_3/P_1}BP_3
115\]
116"#,
117    gold_standard_file: "fourier_series_model.json",
118    category: "Ehlers DSP",
119};
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::traits::Next;
125    use proptest::prelude::*;
126
127    #[test]
128    fn test_fourier_series_basic() {
129        let mut fsm = FourierSeriesModel::new(20);
130        for i in 0..100 {
131            let val = fsm.next(100.0 + (i as f64 * 0.1).sin());
132            assert!(!val.is_nan());
133        }
134    }
135
136    proptest! {
137        #[test]
138        fn test_fourier_series_parity(
139            inputs in prop::collection::vec(1.0..100.0, 100..150),
140        ) {
141            let fundamental = 20;
142            let mut fsm = FourierSeriesModel::new(fundamental);
143            let streaming_results: Vec<f64> = inputs.iter().map(|&x| fsm.next(x)).collect();
144
145            // Batch implementation
146            let mut batch_results = Vec::with_capacity(inputs.len());
147            let mut bp1_obj = BandPass::new(fundamental, 0.1);
148            let mut bp2_obj = BandPass::new(fundamental / 2, 0.1);
149            let mut bp3_obj = BandPass::new(fundamental / 3, 0.1);
150
151            let mut bp1_vals = Vec::new();
152            let mut bp2_vals = Vec::new();
153            let mut bp3_vals = Vec::new();
154            let mut q1_vals = Vec::new();
155            let mut q2_vals = Vec::new();
156            let mut q3_vals = Vec::new();
157
158            let q_scale = fundamental as f64 / (2.0 * PI);
159
160            for (i, &input) in inputs.iter().enumerate() {
161                let bp1 = bp1_obj.next(input);
162                let bp2 = bp2_obj.next(input);
163                let bp3 = bp3_obj.next(input);
164
165                let q1 = q_scale * (bp1 - (if i > 0 { bp1_vals[i-1] } else { 0.0 }));
166                let q2 = q_scale * (bp2 - (if i > 0 { bp2_vals[i-1] } else { 0.0 }));
167                let q3 = q_scale * (bp3 - (if i > 0 { bp3_vals[i-1] } else { 0.0 }));
168
169                bp1_vals.push(bp1);
170                bp2_vals.push(bp2);
171                bp3_vals.push(bp3);
172                q1_vals.push(q1);
173                q2_vals.push(q2);
174                q3_vals.push(q3);
175
176                let mut p1 = 0.0;
177                let mut p2 = 0.0;
178                let mut p3 = 0.0;
179                let start = if i >= fundamental - 1 { i + 1 - fundamental } else { 0 };
180                let _count = i + 1 - start;
181                for j in start..=i {
182                    p1 += bp1_vals[j] * bp1_vals[j] + q1_vals[j] * q1_vals[j];
183                    p2 += bp2_vals[j] * bp2_vals[j] + q2_vals[j] * q2_vals[j];
184                    p3 += bp3_vals[j] * bp3_vals[j] + q3_vals[j] * q3_vals[j];
185                }
186
187                // Normalizing to moving average-like behavior
188                // (Wait, SMA * period is just the sum)
189
190                let mut wave = bp1;
191                if p1 > 0.0 {
192                    wave += (p2 / p1).sqrt() * bp2;
193                    wave += (p3 / p1).sqrt() * bp3;
194                }
195                batch_results.push(wave);
196            }
197
198            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
199                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
200            }
201        }
202    }
203}