Skip to main content

wickra_core/indicators/
sine_wave.rs

1//! Ehlers Sine Wave indicator.
2
3use crate::indicators::ht_dcphase::HtDcPhase;
4use crate::traits::Indicator;
5
6/// Ehlers' Sine Wave indicator (sine + leadsine), TA-Lib `HT_SINE`.
7///
8/// Implementation from *Rocket Science for Traders* (Ehlers 2001, ch. 9). The
9/// phase is the dominant-cycle phase of [`HtDcPhase`]: the smoothed price is
10/// correlated with one cycle of a sine and a cosine of the measured dominant
11/// period, `DCPhase = atan(Real / Imag)`, then corrected (`+90°`, the smoother's
12/// lag, the quadrant). The indicator returns
13///
14/// ```text
15/// Sine     = sin(DCPhase)
16/// LeadSine = sin(DCPhase + 45°)
17/// ```
18///
19/// The two lines cross deep in trends but oscillate rapidly during cycles,
20/// providing a visual lead/lag signal.
21///
22/// Only the primary `sine` line is exposed as the scalar output to match the
23/// crate's standard scalar-indicator surface; the lead is accessible via the
24/// [`SineWave::lead`] accessor after each update.
25///
26/// # Example
27///
28/// ```
29/// use wickra_core::{Indicator, SineWave};
30///
31/// let mut sw = SineWave::new();
32/// let mut last = None;
33/// for i in 0..200 {
34///     last = sw.update(100.0 + (f64::from(i) * 0.4).sin() * 5.0);
35/// }
36/// assert!(last.is_some());
37/// ```
38#[derive(Debug, Clone, Default)]
39pub struct SineWave {
40    phase: HtDcPhase,
41    last_sine: Option<f64>,
42    last_lead: f64,
43}
44
45impl SineWave {
46    /// Construct a new Sine Wave indicator.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Most recent lead (45°-ahead) value. `0.0` until the indicator is ready.
52    pub const fn lead(&self) -> f64 {
53        self.last_lead
54    }
55
56    /// Current sine value if available.
57    pub const fn value(&self) -> Option<f64> {
58        self.last_sine
59    }
60}
61
62impl Indicator for SineWave {
63    type Input = f64;
64    type Output = f64;
65
66    fn update(&mut self, input: f64) -> Option<f64> {
67        if !input.is_finite() {
68            return None;
69        }
70        let phase = self.phase.update(input)?.to_radians();
71        let sine = phase.sin();
72        self.last_lead = (phase + 45f64.to_radians()).sin();
73        self.last_sine = Some(sine);
74        Some(sine)
75    }
76
77    fn reset(&mut self) {
78        self.phase.reset();
79        self.last_sine = None;
80        self.last_lead = 0.0;
81    }
82
83    #[inline]
84    fn warmup_period(&self) -> usize {
85        self.phase.warmup_period()
86    }
87
88    #[inline]
89    fn is_ready(&self) -> bool {
90        self.last_sine.is_some()
91    }
92
93    #[inline]
94    fn name(&self) -> &'static str {
95        "SineWave"
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::traits::BatchExt;
103
104    #[test]
105    fn accessors_and_metadata() {
106        let mut sw = SineWave::new();
107        assert_eq!(sw.warmup_period(), 50);
108        assert_eq!(sw.name(), "SineWave");
109        assert!(!sw.is_ready());
110        assert!(sw.value().is_none());
111        let prices: Vec<f64> = (0..120)
112            .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
113            .collect();
114        sw.batch(&prices);
115        assert!(sw.is_ready());
116        assert!(sw.value().is_some());
117    }
118
119    #[test]
120    fn output_bounded() {
121        let prices: Vec<f64> = (0..200)
122            .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 5.0)
123            .collect();
124        let mut sw = SineWave::new();
125        for v in sw.batch(&prices).into_iter().flatten() {
126            assert!((-1.0..=1.0).contains(&v), "sine out of bounds: {v}");
127        }
128        // Lead value also bounded after warmup.
129        assert!(sw.lead() >= -1.0 && sw.lead() <= 1.0);
130    }
131
132    #[test]
133    fn batch_equals_streaming() {
134        let prices: Vec<f64> = (0..200)
135            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
136            .collect();
137        let mut a = SineWave::new();
138        let mut b = SineWave::new();
139        let batch = a.batch(&prices);
140        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
141        assert_eq!(batch, streamed);
142    }
143
144    #[test]
145    fn ignores_non_finite_input() {
146        let mut sw = SineWave::new();
147        let prices: Vec<f64> = (0..120)
148            .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
149            .collect();
150        sw.batch(&prices);
151        let before = sw.value();
152        assert!(before.is_some());
153        assert_eq!(sw.update(f64::NAN), None);
154    }
155
156    #[test]
157    fn reset_clears_state() {
158        let mut sw = SineWave::new();
159        let prices: Vec<f64> = (0..120)
160            .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
161            .collect();
162        sw.batch(&prices);
163        assert!(sw.is_ready());
164        sw.reset();
165        assert!(!sw.is_ready());
166        assert!(sw.value().is_none());
167    }
168
169    #[test]
170    fn flat_input_uses_phase_fallback() {
171        // Zero inputs make the DC phase's real and imaginary parts exactly
172        // zero, so the phase takes its degenerate-imaginary guard and the
173        // sine is still defined.
174        let mut sw = SineWave::new();
175        let _ = sw.batch(&[0.0_f64; 120]);
176        assert!(sw.value().is_some());
177    }
178
179    use crate::traits::BatchNanExt;
180    use approx::assert_relative_eq;
181
182    fn sine_prices(n: u32) -> Vec<f64> {
183        (0..n)
184            .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
185            .collect()
186    }
187
188    #[test]
189    fn first_value_lands_exactly_at_warmup() {
190        let mut sw = SineWave::new();
191        let out = sw.batch(&sine_prices(120));
192        let warmup = sw.warmup_period();
193        assert!(out[..warmup - 1].iter().all(Option::is_none));
194        assert!(out[warmup - 1].is_some());
195    }
196
197    #[test]
198    fn reset_replays_identically() {
199        let prices = sine_prices(150);
200        let fresh = SineWave::new().batch(&prices);
201        let mut sw = SineWave::new();
202        let first = sw.batch(&prices);
203        sw.reset();
204        assert_eq!(sw.lead().to_bits(), 0.0_f64.to_bits());
205        let second = sw.batch(&prices);
206        assert_eq!(first, fresh);
207        assert_eq!(second, fresh);
208    }
209
210    #[test]
211    fn batch_nan_paths_match_streaming_bitwise() {
212        let prices = sine_prices(150);
213        let mut out = vec![0.0; prices.len()];
214        SineWave::new().batch_nan_into(&prices, &mut out);
215        let nan = SineWave::new().batch_nan(&prices);
216        let fast = SineWave::new().batch_fast(&prices);
217        let mut stream = SineWave::new();
218        let expected: Vec<u64> = prices
219            .iter()
220            .map(|&p| stream.update(p).unwrap_or(f64::NAN).to_bits())
221            .collect();
222        assert!(out.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
223        assert!(nan.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
224        assert!(fast.iter().zip(&expected).all(|(v, e)| v.to_bits() == *e));
225    }
226
227    #[test]
228    fn sine_and_lead_are_functions_of_dc_phase() {
229        // sine = sin(phase), lead = sin(phase + 45 deg), where phase is HT_DCPHASE.
230        let prices = sine_prices(150);
231        let mut sw = SineWave::new();
232        let mut phase = HtDcPhase::new();
233        for &p in &prices {
234            let sine = sw.update(p);
235            let ph = phase.update(p);
236            assert_eq!(sine.is_some(), ph.is_some());
237            if let (Some(s), Some(deg)) = (sine, ph) {
238                let rad = deg.to_radians();
239                assert_eq!(s.to_bits(), rad.sin().to_bits());
240                assert_eq!(
241                    sw.lead().to_bits(),
242                    (rad + 45f64.to_radians()).sin().to_bits()
243                );
244            }
245        }
246    }
247
248    #[test]
249    fn zero_series_hand_computed() {
250        // On a zero series HT_DCPHASE converges to 90 + 90 + 360/6 = 240 degrees,
251        // so sine = sin(240 deg) = -sqrt(3)/2 = -0.866_025_4 and
252        // lead = sin(285 deg) = -(sqrt(6) + sqrt(2))/4 = -0.965_925_8.
253        let mut sw = SineWave::new();
254        let _ = sw.batch(&[0.0; 400]);
255        assert_relative_eq!(sw.value().unwrap(), -(3.0_f64.sqrt()) / 2.0, epsilon = 1e-6);
256        assert_relative_eq!(
257            sw.lead(),
258            -(6.0_f64.sqrt() + 2.0_f64.sqrt()) / 4.0,
259            epsilon = 1e-6
260        );
261    }
262}