Skip to main content

quantwave_core/indicators/
sine_wave.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::indicators::hilbert_transform::{HilbertFIR, EhlersWma4};
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// Sine Wave Indicator
7///
8/// Based on John Ehlers' "Rocket Science for Traders" (Chapter 9).
9/// Uses the phase of the Hilbert Transform to plot a sine wave and a lead-sine wave.
10/// Returns (Sine, LeadSine).
11#[derive(Debug, Clone)]
12pub struct SineWave {
13    wma_price: EhlersWma4,
14    hilbert_detrender: HilbertFIR,
15    hilbert_q1: HilbertFIR,
16    
17    detrender_history: VecDeque<f64>,
18    period_prev: f64,
19    count: usize,
20}
21
22impl SineWave {
23    pub fn new() -> Self {
24        Self {
25            wma_price: EhlersWma4::new(),
26            hilbert_detrender: HilbertFIR::new(),
27            hilbert_q1: HilbertFIR::new(),
28            
29            detrender_history: VecDeque::from(vec![0.0; 7]),
30            period_prev: 6.0,
31            count: 0,
32        }
33    }
34}
35
36impl Default for SineWave {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Next<f64> for SineWave {
43    type Output = (f64, f64);
44
45    fn next(&mut self, price: f64) -> Self::Output {
46        self.count += 1;
47
48        if self.count < 7 {
49            self.wma_price.next(price);
50            return (0.0, 0.0);
51        }
52
53        let smooth = self.wma_price.next(price);
54        let detrender = self.hilbert_detrender.next(smooth, self.period_prev);
55        
56        self.detrender_history.pop_back();
57        self.detrender_history.push_front(detrender);
58
59        let q1 = self.hilbert_q1.next(detrender, self.period_prev);
60        let i1 = self.detrender_history[3];
61
62        // Simple Phase calculation as per Chapter 9
63        let mut phase = 0.0;
64        if i1.abs() > 0.0001 {
65            phase = (q1 / i1).atan().to_degrees();
66        }
67
68        let sine = phase.to_radians().sin();
69        let lead_sine = (phase + 45.0).to_radians().sin();
70
71        (sine, lead_sine)
72    }
73}
74
75pub const SINE_WAVE_METADATA: IndicatorMetadata = IndicatorMetadata {
76    name: "Sine Wave",
77    description: "Plots a sine wave and a lead-sine wave based on the cyclic phase of price movement.",
78    usage: "Use to confirm whether the market is in cycle or trend mode. When price follows the sine wave trade cycle reversals; when it diverges switch to trend-following.",
79    keywords: &["cycle", "oscillator", "ehlers", "dsp", "phase"],
80    ehlers_summary: "Introduced in Rocket Science for Traders, the Sine Wave Indicator plots the sine and cosine of measured instantaneous phase. In cycling markets price tracks the sine wave; in trending markets price breaks through the lead line signaling a mode change.",
81    params: &[],
82    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/ROCKET%20SCIENCE%20FOR%20TRADER.pdf",
83    formula_latex: r#"
84\[
85\text{Sine} = \sin(\text{Phase})
86\]
87\[
88\text{LeadSine} = \sin(\text{Phase} + 45^\circ)
89\]
90"#,
91    gold_standard_file: "sine_wave.json",
92    category: "Rocket Science",
93};
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::traits::Next;
99    use proptest::prelude::*;
100
101    #[test]
102    fn test_sine_wave_basic() {
103        let mut sw = SineWave::new();
104        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0];
105        for p in prices {
106            let (s, l) = sw.next(p);
107            assert!(!s.is_nan());
108            assert!(!l.is_nan());
109        }
110    }
111
112    proptest! {
113        #[test]
114        fn test_sine_wave_parity(
115            inputs in prop::collection::vec(1.0..100.0, 50..100),
116        ) {
117            let mut sw = SineWave::new();
118            let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| sw.next(x)).collect();
119
120            let mut sw_batch = SineWave::new();
121            let batch_results: Vec<(f64, f64)> = inputs.iter().map(|&x| sw_batch.next(x)).collect();
122
123            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
124                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
125                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
126            }
127        }
128    }
129}