Skip to main content

wickra_core/indicators/
generalized_dema.rs

1//! Generalized DEMA (GD) — Tim Tillson's volume-factor double EMA.
2
3use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7/// Generalized DEMA — the building block of Tillson's [`T3`](crate::T3),
8/// exposed on its own.
9///
10/// ```text
11/// GD = (1 + v) · EMA(price) − v · EMA(EMA(price))
12/// ```
13///
14/// where both EMAs share the same `period` and `v ∈ [0, 1]` is the *volume
15/// factor*. `v` controls how much of the second-order lag correction is
16/// applied:
17///
18/// - `v = 0` collapses GD to a plain [`Ema`](crate::Ema) (no correction).
19/// - `v = 1` recovers the standard [`Dema`](crate::Dema) `2·EMA − EMA(EMA)`.
20/// - intermediate values (Tillson uses `0.7`) trade a little lag reduction for
21///   less overshoot than DEMA.
22///
23/// Because the coefficients `(1 + v)` and `−v` always sum to `1`, a constant
24/// series maps to itself. The first output lands after `2·period − 1` inputs —
25/// EMA1 seeds at `period`, then EMA2 needs another `period − 1` of EMA1's
26/// outputs to seed, exactly like DEMA.
27///
28/// # Example
29///
30/// ```
31/// use wickra_core::{Indicator, GeneralizedDema};
32///
33/// let mut indicator = GeneralizedDema::new(5, 0.7).unwrap();
34/// let mut last = None;
35/// for i in 0..80 {
36///     last = indicator.update(100.0 + f64::from(i));
37/// }
38/// assert!(last.is_some());
39/// ```
40#[derive(Debug, Clone)]
41pub struct GeneralizedDema {
42    ema1: Ema,
43    ema2: Ema,
44    period: usize,
45    v: f64,
46}
47
48impl GeneralizedDema {
49    /// Construct a generalized DEMA with the given `period` and volume factor
50    /// `v`.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`Error::PeriodZero`] if `period == 0`, or
55    /// [`Error::InvalidPeriod`] if `v` is non-finite or outside `[0.0, 1.0]`.
56    pub fn new(period: usize, v: f64) -> Result<Self> {
57        if period == 0 {
58            return Err(Error::PeriodZero);
59        }
60        if period > crate::error::MAX_PERIOD {
61            return Err(Error::InvalidPeriod {
62                message: crate::error::PERIOD_ABOVE_MAX,
63            });
64        }
65        if !v.is_finite() || !(0.0..=1.0).contains(&v) {
66            return Err(Error::InvalidPeriod {
67                message: "GD volume factor must be a finite value in [0.0, 1.0]",
68            });
69        }
70        Ok(Self {
71            ema1: Ema::new(period)?,
72            ema2: Ema::new(period)?,
73            period,
74            v,
75        })
76    }
77
78    /// Configured period.
79    pub const fn period(&self) -> usize {
80        self.period
81    }
82
83    /// Configured volume factor `v`.
84    pub const fn volume_factor(&self) -> f64 {
85        self.v
86    }
87}
88
89impl Indicator for GeneralizedDema {
90    type Input = f64;
91    type Output = f64;
92
93    #[inline]
94    fn update(&mut self, input: f64) -> Option<f64> {
95        let e1 = self.ema1.update(input)?;
96        let e2 = self.ema2.update(e1)?;
97        Some((1.0 + self.v) * e1 - self.v * e2)
98    }
99
100    fn reset(&mut self) {
101        self.ema1.reset();
102        self.ema2.reset();
103    }
104
105    #[inline]
106    fn warmup_period(&self) -> usize {
107        // EMA1 seeds at period, then EMA2 needs another (period - 1) values.
108        2 * self.period - 1
109    }
110
111    #[inline]
112    fn is_ready(&self) -> bool {
113        self.ema2.is_ready()
114    }
115
116    #[inline]
117    fn name(&self) -> &'static str {
118        "GD"
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::indicators::Dema;
126    use crate::traits::BatchExt;
127    use approx::assert_relative_eq;
128
129    #[test]
130    fn rejects_zero_period() {
131        assert!(matches!(
132            GeneralizedDema::new(0, 0.7),
133            Err(Error::PeriodZero)
134        ));
135    }
136
137    #[test]
138    fn rejects_invalid_volume_factor() {
139        assert!(matches!(
140            GeneralizedDema::new(5, -0.1),
141            Err(Error::InvalidPeriod { .. })
142        ));
143        assert!(matches!(
144            GeneralizedDema::new(5, 1.5),
145            Err(Error::InvalidPeriod { .. })
146        ));
147        assert!(matches!(
148            GeneralizedDema::new(5, f64::NAN),
149            Err(Error::InvalidPeriod { .. })
150        ));
151        assert!(GeneralizedDema::new(5, 0.0).is_ok());
152        assert!(GeneralizedDema::new(5, 1.0).is_ok());
153    }
154
155    /// Cover the const accessors `period` + `volume_factor` and the
156    /// Indicator-impl `warmup_period` + `name`.
157    #[test]
158    fn accessors_and_metadata() {
159        let gd = GeneralizedDema::new(5, 0.7).unwrap();
160        assert_eq!(gd.period(), 5);
161        assert_relative_eq!(gd.volume_factor(), 0.7, epsilon = 1e-12);
162        // EMA1 seeds at 5, EMA2 needs another 4 -> 2*period - 1 = 9.
163        assert_eq!(gd.warmup_period(), 9);
164        assert_eq!(gd.name(), "GD");
165    }
166
167    #[test]
168    fn constant_series_yields_constant() {
169        let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
170        let out = gd.batch(&[100.0_f64; 60]);
171        let last = out.iter().rev().flatten().next().unwrap();
172        assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
173    }
174
175    #[test]
176    fn v_one_equals_dema() {
177        // GD with v = 1 is exactly the standard DEMA.
178        let prices: Vec<f64> = (1..=80)
179            .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
180            .collect();
181        let mut gd = GeneralizedDema::new(7, 1.0).unwrap();
182        let mut dema = Dema::new(7).unwrap();
183        let gd_out = gd.batch(&prices);
184        let dema_out = dema.batch(&prices);
185        for (g, d) in gd_out.iter().zip(dema_out.iter()) {
186            assert_eq!(g.is_some(), d.is_some());
187            if let (Some(a), Some(b)) = (g, d) {
188                assert_relative_eq!(*a, *b, epsilon = 1e-9);
189            }
190        }
191    }
192
193    #[test]
194    fn v_zero_equals_ema() {
195        // GD with v = 0 is a plain EMA (no second-order correction).
196        let prices: Vec<f64> = (1..=60).map(|i| f64::from(i) * 0.5).collect();
197        let mut gd = GeneralizedDema::new(6, 0.0).unwrap();
198        let mut ema = Ema::new(6).unwrap();
199        let gd_out = gd.batch(&prices);
200        for (i, (g, p)) in gd_out.iter().zip(prices.iter()).enumerate() {
201            // GD(v=0) feeds EMA1 into EMA2 but outputs EMA1 alone (coefficient
202            // 1 on e1, 0 on e2); it is only ready once EMA2 is, so compare
203            // against a standalone EMA chained the same way.
204            let want = ema.update(*p).filter(|_| i + 1 >= gd.warmup_period());
205            if let (Some(a), Some(b)) = (g, want) {
206                assert_relative_eq!(*a, b, epsilon = 1e-9);
207            }
208        }
209    }
210
211    #[test]
212    fn batch_equals_streaming() {
213        let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
214        let mut a = GeneralizedDema::new(7, 0.7).unwrap();
215        let mut b = GeneralizedDema::new(7, 0.7).unwrap();
216        assert_eq!(
217            a.batch(&prices),
218            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
219        );
220    }
221
222    #[test]
223    fn reset_clears_state() {
224        let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
225        gd.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
226        assert!(gd.is_ready());
227        gd.reset();
228        assert!(!gd.is_ready());
229        assert_eq!(gd.update(1.0), None);
230    }
231}