Skip to main content

wickra_core/indicators/
even_better_sinewave.rs

1//! Ehlers Even Better Sinewave (EBSW) — a normalised cycle oscillator in [-1, 1].
2#![allow(clippy::doc_markdown)]
3
4use std::f64::consts::PI;
5
6use crate::error::{Error, Result};
7use crate::indicators::super_smoother::SuperSmoother;
8use crate::traits::Indicator;
9
10/// Ehlers' **Even Better Sinewave** (EBSW) — a self-normalising cycle oscillator
11/// that swings cleanly in `[−1, +1]` regardless of price amplitude.
12///
13/// From John Ehlers' *Cycle Analytics for Traders* (2013, ch. 12):
14///
15/// ```text
16/// alpha1 = (1 − sin(2π/hp_period)) / cos(2π/hp_period)
17/// HP_t   = 0.5·(1 + alpha1)·(price_t − price_{t−1}) + alpha1·HP_{t−1}   (one-pole highpass)
18/// Filt   = SuperSmoother(HP, ssf_length)
19/// Wave   = (Filt_t + Filt_{t−1} + Filt_{t−2}) / 3
20/// Pwr    = (Filt_t² + Filt_{t−1}² + Filt_{t−2}²) / 3
21/// EBSW   = Wave / sqrt(Pwr)
22/// ```
23///
24/// The price is first highpass-filtered to remove the trend, then SuperSmoothed to
25/// remove noise, leaving the dominant cycle. Dividing a 3-bar average of that
26/// cycle by its RMS power normalises the amplitude, so the output reads like a
27/// clean sine wave bounded in `[−1, +1]` whatever the instrument. Unlike the
28/// classic [`SineWave`](crate::SineWave) (which derives in-phase/quadrature
29/// components from the Hilbert transform and can whip in trends), the EBSW stays
30/// well-behaved and is read directly: crossing up through `0`/`−0.9` is a buy
31/// cue, crossing down through `0`/`+0.9` a sell cue.
32///
33/// The first value lands once three SuperSmoothed samples exist
34/// (`warmup_period == 3`). Each `update` is O(1).
35///
36/// # Example
37///
38/// ```
39/// use wickra_core::{Indicator, EvenBetterSinewave};
40///
41/// let mut indicator = EvenBetterSinewave::new(40, 10).unwrap();
42/// let mut last = None;
43/// for i in 0..120 {
44///     last = indicator.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
45/// }
46/// assert!(last.is_some());
47/// ```
48#[derive(Debug, Clone)]
49pub struct EvenBetterSinewave {
50    hp_period: usize,
51    ssf_length: usize,
52    alpha1: f64,
53    smoother: SuperSmoother,
54    prev_price: Option<f64>,
55    hp: f64,
56    filt1: Option<f64>,
57    filt2: Option<f64>,
58    filt3: Option<f64>,
59    last: Option<f64>,
60}
61
62impl EvenBetterSinewave {
63    /// Construct an EBSW with the given highpass `hp_period` and SuperSmoother
64    /// `ssf_length`.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`Error::PeriodZero`] if either argument is `0`.
69    pub fn new(hp_period: usize, ssf_length: usize) -> Result<Self> {
70        if hp_period == 0 || ssf_length == 0 {
71            return Err(Error::PeriodZero);
72        }
73        let w = 2.0 * PI / hp_period as f64;
74        let alpha1 = (1.0 - w.sin()) / w.cos();
75        Ok(Self {
76            hp_period,
77            ssf_length,
78            alpha1,
79            smoother: SuperSmoother::new(ssf_length)?,
80            prev_price: None,
81            hp: 0.0,
82            filt1: None,
83            filt2: None,
84            filt3: None,
85            last: None,
86        })
87    }
88
89    /// Configured `(hp_period, ssf_length)`.
90    pub const fn params(&self) -> (usize, usize) {
91        (self.hp_period, self.ssf_length)
92    }
93
94    /// Current value if available.
95    pub const fn value(&self) -> Option<f64> {
96        self.last
97    }
98}
99
100impl Indicator for EvenBetterSinewave {
101    type Input = f64;
102    type Output = f64;
103
104    fn update(&mut self, price: f64) -> Option<f64> {
105        if !price.is_finite() {
106            return self.last;
107        }
108        let hp = match self.prev_price {
109            Some(prev) => 0.5 * (1.0 + self.alpha1) * (price - prev) + self.alpha1 * self.hp,
110            None => 0.0,
111        };
112        self.prev_price = Some(price);
113        self.hp = hp;
114        let filt = self.smoother.update(hp)?;
115        // Shift the three-deep filter buffer.
116        self.filt3 = self.filt2;
117        self.filt2 = self.filt1;
118        self.filt1 = Some(filt);
119        let (Some(f1), Some(f2), Some(f3)) = (self.filt1, self.filt2, self.filt3) else {
120            return None;
121        };
122        let wave = (f1 + f2 + f3) / 3.0;
123        let pwr = (f1 * f1 + f2 * f2 + f3 * f3) / 3.0;
124        let ebsw = if pwr > 0.0 {
125            (wave / pwr.sqrt()).clamp(-1.0, 1.0)
126        } else {
127            0.0
128        };
129        self.last = Some(ebsw);
130        Some(ebsw)
131    }
132
133    fn reset(&mut self) {
134        self.smoother.reset();
135        self.prev_price = None;
136        self.hp = 0.0;
137        self.filt1 = None;
138        self.filt2 = None;
139        self.filt3 = None;
140        self.last = None;
141    }
142
143    fn warmup_period(&self) -> usize {
144        3
145    }
146
147    fn is_ready(&self) -> bool {
148        self.last.is_some()
149    }
150
151    fn name(&self) -> &'static str {
152        "EvenBetterSinewave"
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::traits::BatchExt;
160
161    #[test]
162    fn rejects_zero_params() {
163        assert!(matches!(
164            EvenBetterSinewave::new(0, 10),
165            Err(Error::PeriodZero)
166        ));
167        assert!(matches!(
168            EvenBetterSinewave::new(40, 0),
169            Err(Error::PeriodZero)
170        ));
171    }
172
173    #[test]
174    fn accessors_and_metadata() {
175        let e = EvenBetterSinewave::new(40, 10).unwrap();
176        assert_eq!(e.params(), (40, 10));
177        assert_eq!(e.warmup_period(), 3);
178        assert_eq!(e.name(), "EvenBetterSinewave");
179        assert!(!e.is_ready());
180        assert_eq!(e.value(), None);
181    }
182
183    #[test]
184    fn first_emission_at_warmup_period() {
185        let mut e = EvenBetterSinewave::new(40, 10).unwrap();
186        let xs: Vec<f64> = (0..12)
187            .map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 3.0)
188            .collect();
189        let out = e.batch(&xs);
190        for v in out.iter().take(2) {
191            assert!(v.is_none());
192        }
193        assert!(out[2].is_some());
194    }
195
196    #[test]
197    fn output_in_range() {
198        let mut e = EvenBetterSinewave::new(40, 10).unwrap();
199        let xs: Vec<f64> = (0..400)
200            .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
201            .collect();
202        for v in e.batch(&xs).into_iter().flatten() {
203            assert!((-1.0..=1.0).contains(&v), "EBSW out of range: {v}");
204        }
205    }
206
207    #[test]
208    fn cyclic_input_swings_both_signs() {
209        let mut e = EvenBetterSinewave::new(30, 8).unwrap();
210        let xs: Vec<f64> = (0..400)
211            .map(|i| 100.0 + (std::f64::consts::TAU * f64::from(i) / 30.0).sin() * 5.0)
212            .collect();
213        let out: Vec<f64> = e.batch(&xs).into_iter().flatten().skip(100).collect();
214        assert!(out.iter().any(|&v| v > 0.5));
215        assert!(out.iter().any(|&v| v < -0.5));
216    }
217
218    #[test]
219    fn ignores_non_finite() {
220        let mut e = EvenBetterSinewave::new(40, 10).unwrap();
221        e.batch(
222            &(0..40)
223                .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
224                .collect::<Vec<_>>(),
225        );
226        let before = e.value();
227        assert_eq!(e.update(f64::NAN), before);
228    }
229
230    #[test]
231    fn reset_clears_state() {
232        let mut e = EvenBetterSinewave::new(40, 10).unwrap();
233        e.batch(
234            &(0..40)
235                .map(|i| 100.0 + (f64::from(i) * 0.3).sin())
236                .collect::<Vec<_>>(),
237        );
238        assert!(e.is_ready());
239        e.reset();
240        assert!(!e.is_ready());
241        assert_eq!(e.value(), None);
242    }
243
244    #[test]
245    fn batch_equals_streaming() {
246        let xs: Vec<f64> = (0..120)
247            .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
248            .collect();
249        let batch = EvenBetterSinewave::new(40, 10).unwrap().batch(&xs);
250        let mut b = EvenBetterSinewave::new(40, 10).unwrap();
251        let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
252        assert_eq!(batch, streamed);
253    }
254
255    #[test]
256    fn flat_input_yields_zero_power() {
257        // A constant series drives the highpass/smoother outputs to zero, so the
258        // signal power is zero and the oscillator reports 0.0 (the `pwr == 0` arm).
259        let flat = [100.0_f64; 200];
260        let last = EvenBetterSinewave::new(40, 10)
261            .unwrap()
262            .batch(&flat)
263            .into_iter()
264            .flatten()
265            .last()
266            .unwrap();
267        assert_eq!(last, 0.0);
268    }
269}