Skip to main content

quantwave_core/indicators/
fourier_series.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::bandpass::BandPass;
4use crate::indicators::smoothing::SMA;
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: &[
98        ParamDef {
99            name: "fundamental",
100            default: "20",
101            description: "Fundamental cycle period",
102        },
103    ],
104    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/FOURIER%20SERIES%20MODEL%20OF%20THE%20MARKET.pdf",
105    formula_latex: r#"
106\[
107BP_k = \text{BandPass}(Price, Fundamental/k)
108\]
109\[
110Q_k = \frac{Fundamental}{2\pi} (BP_{k} - BP_{k,t-1})
111\]
112\[
113P_k = \sum_{n=0}^{F-1} (BP_{k,t-n}^2 + Q_{k,t-n}^2)
114\]
115\[
116Wave = BP_1 + \sqrt{P_2/P_1}BP_2 + \sqrt{P_3/P_1}BP_3
117\]
118"#,
119    gold_standard_file: "fourier_series_model.json",
120    category: "Ehlers DSP",
121};
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::traits::Next;
127    use proptest::prelude::*;
128
129    #[test]
130    fn test_fourier_series_basic() {
131        let mut fsm = FourierSeriesModel::new(20);
132        for i in 0..100 {
133            let val = fsm.next(100.0 + (i as f64 * 0.1).sin());
134            assert!(!val.is_nan());
135        }
136    }
137
138    proptest! {
139        #[test]
140        fn test_fourier_series_parity(
141            inputs in prop::collection::vec(1.0..100.0, 100..150),
142        ) {
143            let fundamental = 20;
144            let mut fsm = FourierSeriesModel::new(fundamental);
145            let streaming_results: Vec<f64> = inputs.iter().map(|&x| fsm.next(x)).collect();
146
147            // Batch implementation
148            let mut batch_results = Vec::with_capacity(inputs.len());
149            let mut bp1_obj = BandPass::new(fundamental, 0.1);
150            let mut bp2_obj = BandPass::new(fundamental / 2, 0.1);
151            let mut bp3_obj = BandPass::new(fundamental / 3, 0.1);
152            
153            let mut bp1_vals = Vec::new();
154            let mut bp2_vals = Vec::new();
155            let mut bp3_vals = Vec::new();
156            let mut q1_vals = Vec::new();
157            let mut q2_vals = Vec::new();
158            let mut q3_vals = Vec::new();
159
160            let q_scale = fundamental as f64 / (2.0 * PI);
161
162            for (i, &input) in inputs.iter().enumerate() {
163                let bp1 = bp1_obj.next(input);
164                let bp2 = bp2_obj.next(input);
165                let bp3 = bp3_obj.next(input);
166
167                let q1 = q_scale * (bp1 - (if i > 0 { bp1_vals[i-1] } else { 0.0 }));
168                let q2 = q_scale * (bp2 - (if i > 0 { bp2_vals[i-1] } else { 0.0 }));
169                let q3 = q_scale * (bp3 - (if i > 0 { bp3_vals[i-1] } else { 0.0 }));
170
171                bp1_vals.push(bp1);
172                bp2_vals.push(bp2);
173                bp3_vals.push(bp3);
174                q1_vals.push(q1);
175                q2_vals.push(q2);
176                q3_vals.push(q3);
177
178                let mut p1 = 0.0;
179                let mut p2 = 0.0;
180                let mut p3 = 0.0;
181                let start = if i >= fundamental - 1 { i + 1 - fundamental } else { 0 };
182                let _count = i + 1 - start;
183                for j in start..=i {
184                    p1 += bp1_vals[j] * bp1_vals[j] + q1_vals[j] * q1_vals[j];
185                    p2 += bp2_vals[j] * bp2_vals[j] + q2_vals[j] * q2_vals[j];
186                    p3 += bp3_vals[j] * bp3_vals[j] + q3_vals[j] * q3_vals[j];
187                }
188                
189                // Normalizing to moving average-like behavior
190                // (Wait, SMA * period is just the sum)
191                
192                let mut wave = bp1;
193                if p1 > 0.0 {
194                    wave += (p2 / p1).sqrt() * bp2;
195                    wave += (p3 / p1).sqrt() * bp3;
196                }
197                batch_results.push(wave);
198            }
199
200            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
201                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
202            }
203        }
204    }
205}