use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use crate::indicators::bandpass::BandPass;
use crate::indicators::smoothing::SMA;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct FourierSeriesModel {
fundamental: usize,
bp1: BandPass,
bp2: BandPass,
bp3: BandPass,
bp1_prev: f64,
bp2_prev: f64,
bp3_prev: f64,
p1_sma: SMA,
p2_sma: SMA,
p3_sma: SMA,
count: usize,
}
impl FourierSeriesModel {
pub fn new(fundamental: usize) -> Self {
Self {
fundamental,
bp1: BandPass::new(fundamental, 0.1),
bp2: BandPass::new(fundamental / 2, 0.1),
bp3: BandPass::new(fundamental / 3, 0.1),
bp1_prev: 0.0,
bp2_prev: 0.0,
bp3_prev: 0.0,
p1_sma: SMA::new(fundamental),
p2_sma: SMA::new(fundamental),
p3_sma: SMA::new(fundamental),
count: 0,
}
}
}
impl Default for FourierSeriesModel {
fn default() -> Self {
Self::new(20)
}
}
impl Next<f64> for FourierSeriesModel {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
self.count += 1;
let bp1 = self.bp1.next(input);
let bp2 = self.bp2.next(input);
let bp3 = self.bp3.next(input);
let q_scale = self.fundamental as f64 / (2.0 * PI);
let q1 = q_scale * (bp1 - self.bp1_prev);
let q2 = q_scale * (bp2 - self.bp2_prev);
let q3 = q_scale * (bp3 - self.bp3_prev);
let p1 = self.p1_sma.next(bp1 * bp1 + q1 * q1) * self.fundamental as f64;
let p2 = self.p2_sma.next(bp2 * bp2 + q2 * q2) * self.fundamental as f64;
let p3 = self.p3_sma.next(bp3 * bp3 + q3 * q3) * self.fundamental as f64;
self.bp1_prev = bp1;
self.bp2_prev = bp2;
self.bp3_prev = bp3;
let mut wave = bp1;
if p1 > 0.0 {
wave += (p2 / p1).sqrt() * bp2;
wave += (p3 / p1).sqrt() * bp3;
}
wave
}
}
pub const FOURIER_SERIES_MODEL_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "FourierSeriesModel",
description: "Synthesized market model using fundamental and harmonic frequency components.",
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.",
keywords: &["cycle", "spectral", "ehlers", "prediction", "fourier"],
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.",
params: &[
ParamDef {
name: "fundamental",
default: "20",
description: "Fundamental cycle period",
},
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/FOURIER%20SERIES%20MODEL%20OF%20THE%20MARKET.pdf",
formula_latex: r#"
\[
BP_k = \text{BandPass}(Price, Fundamental/k)
\]
\[
Q_k = \frac{Fundamental}{2\pi} (BP_{k} - BP_{k,t-1})
\]
\[
P_k = \sum_{n=0}^{F-1} (BP_{k,t-n}^2 + Q_{k,t-n}^2)
\]
\[
Wave = BP_1 + \sqrt{P_2/P_1}BP_2 + \sqrt{P_3/P_1}BP_3
\]
"#,
gold_standard_file: "fourier_series_model.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_fourier_series_basic() {
let mut fsm = FourierSeriesModel::new(20);
for i in 0..100 {
let val = fsm.next(100.0 + (i as f64 * 0.1).sin());
assert!(!val.is_nan());
}
}
proptest! {
#[test]
fn test_fourier_series_parity(
inputs in prop::collection::vec(1.0..100.0, 100..150),
) {
let fundamental = 20;
let mut fsm = FourierSeriesModel::new(fundamental);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| fsm.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let mut bp1_obj = BandPass::new(fundamental, 0.1);
let mut bp2_obj = BandPass::new(fundamental / 2, 0.1);
let mut bp3_obj = BandPass::new(fundamental / 3, 0.1);
let mut bp1_vals = Vec::new();
let mut bp2_vals = Vec::new();
let mut bp3_vals = Vec::new();
let mut q1_vals = Vec::new();
let mut q2_vals = Vec::new();
let mut q3_vals = Vec::new();
let q_scale = fundamental as f64 / (2.0 * PI);
for (i, &input) in inputs.iter().enumerate() {
let bp1 = bp1_obj.next(input);
let bp2 = bp2_obj.next(input);
let bp3 = bp3_obj.next(input);
let q1 = q_scale * (bp1 - (if i > 0 { bp1_vals[i-1] } else { 0.0 }));
let q2 = q_scale * (bp2 - (if i > 0 { bp2_vals[i-1] } else { 0.0 }));
let q3 = q_scale * (bp3 - (if i > 0 { bp3_vals[i-1] } else { 0.0 }));
bp1_vals.push(bp1);
bp2_vals.push(bp2);
bp3_vals.push(bp3);
q1_vals.push(q1);
q2_vals.push(q2);
q3_vals.push(q3);
let mut p1 = 0.0;
let mut p2 = 0.0;
let mut p3 = 0.0;
let start = if i >= fundamental - 1 { i + 1 - fundamental } else { 0 };
let _count = i + 1 - start;
for j in start..=i {
p1 += bp1_vals[j] * bp1_vals[j] + q1_vals[j] * q1_vals[j];
p2 += bp2_vals[j] * bp2_vals[j] + q2_vals[j] * q2_vals[j];
p3 += bp3_vals[j] * bp3_vals[j] + q3_vals[j] * q3_vals[j];
}
let mut wave = bp1;
if p1 > 0.0 {
wave += (p2 / p1).sqrt() * bp2;
wave += (p3 / p1).sqrt() * bp3;
}
batch_results.push(wave);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}