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 !v.is_finite() || !(0.0..=1.0).contains(&v) {
61            return Err(Error::InvalidPeriod {
62                message: "GD volume factor must be a finite value in [0.0, 1.0]",
63            });
64        }
65        Ok(Self {
66            ema1: Ema::new(period)?,
67            ema2: Ema::new(period)?,
68            period,
69            v,
70        })
71    }
72
73    /// Configured period.
74    pub const fn period(&self) -> usize {
75        self.period
76    }
77
78    /// Configured volume factor `v`.
79    pub const fn volume_factor(&self) -> f64 {
80        self.v
81    }
82}
83
84impl Indicator for GeneralizedDema {
85    type Input = f64;
86    type Output = f64;
87
88    fn update(&mut self, input: f64) -> Option<f64> {
89        let e1 = self.ema1.update(input)?;
90        let e2 = self.ema2.update(e1)?;
91        Some((1.0 + self.v) * e1 - self.v * e2)
92    }
93
94    fn reset(&mut self) {
95        self.ema1.reset();
96        self.ema2.reset();
97    }
98
99    fn warmup_period(&self) -> usize {
100        // EMA1 seeds at period, then EMA2 needs another (period - 1) values.
101        2 * self.period - 1
102    }
103
104    fn is_ready(&self) -> bool {
105        self.ema2.is_ready()
106    }
107
108    fn name(&self) -> &'static str {
109        "GD"
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::indicators::Dema;
117    use crate::traits::BatchExt;
118    use approx::assert_relative_eq;
119
120    #[test]
121    fn rejects_zero_period() {
122        assert!(matches!(
123            GeneralizedDema::new(0, 0.7),
124            Err(Error::PeriodZero)
125        ));
126    }
127
128    #[test]
129    fn rejects_invalid_volume_factor() {
130        assert!(matches!(
131            GeneralizedDema::new(5, -0.1),
132            Err(Error::InvalidPeriod { .. })
133        ));
134        assert!(matches!(
135            GeneralizedDema::new(5, 1.5),
136            Err(Error::InvalidPeriod { .. })
137        ));
138        assert!(matches!(
139            GeneralizedDema::new(5, f64::NAN),
140            Err(Error::InvalidPeriod { .. })
141        ));
142        assert!(GeneralizedDema::new(5, 0.0).is_ok());
143        assert!(GeneralizedDema::new(5, 1.0).is_ok());
144    }
145
146    /// Cover the const accessors `period` + `volume_factor` and the
147    /// Indicator-impl `warmup_period` + `name`.
148    #[test]
149    fn accessors_and_metadata() {
150        let gd = GeneralizedDema::new(5, 0.7).unwrap();
151        assert_eq!(gd.period(), 5);
152        assert_relative_eq!(gd.volume_factor(), 0.7, epsilon = 1e-12);
153        // EMA1 seeds at 5, EMA2 needs another 4 -> 2*period - 1 = 9.
154        assert_eq!(gd.warmup_period(), 9);
155        assert_eq!(gd.name(), "GD");
156    }
157
158    #[test]
159    fn constant_series_yields_constant() {
160        let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
161        let out = gd.batch(&[100.0_f64; 60]);
162        let last = out.iter().rev().flatten().next().unwrap();
163        assert_relative_eq!(*last, 100.0, epsilon = 1e-9);
164    }
165
166    #[test]
167    fn v_one_equals_dema() {
168        // GD with v = 1 is exactly the standard DEMA.
169        let prices: Vec<f64> = (1..=80)
170            .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
171            .collect();
172        let mut gd = GeneralizedDema::new(7, 1.0).unwrap();
173        let mut dema = Dema::new(7).unwrap();
174        let gd_out = gd.batch(&prices);
175        let dema_out = dema.batch(&prices);
176        for (g, d) in gd_out.iter().zip(dema_out.iter()) {
177            assert_eq!(g.is_some(), d.is_some());
178            if let (Some(a), Some(b)) = (g, d) {
179                assert_relative_eq!(*a, *b, epsilon = 1e-9);
180            }
181        }
182    }
183
184    #[test]
185    fn v_zero_equals_ema() {
186        // GD with v = 0 is a plain EMA (no second-order correction).
187        let prices: Vec<f64> = (1..=60).map(|i| f64::from(i) * 0.5).collect();
188        let mut gd = GeneralizedDema::new(6, 0.0).unwrap();
189        let mut ema = Ema::new(6).unwrap();
190        let gd_out = gd.batch(&prices);
191        for (i, (g, p)) in gd_out.iter().zip(prices.iter()).enumerate() {
192            // GD(v=0) feeds EMA1 into EMA2 but outputs EMA1 alone (coefficient
193            // 1 on e1, 0 on e2); it is only ready once EMA2 is, so compare
194            // against a standalone EMA chained the same way.
195            let want = ema.update(*p).filter(|_| i + 1 >= gd.warmup_period());
196            if let (Some(a), Some(b)) = (g, want) {
197                assert_relative_eq!(*a, b, epsilon = 1e-9);
198            }
199        }
200    }
201
202    #[test]
203    fn batch_equals_streaming() {
204        let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 0.5).collect();
205        let mut a = GeneralizedDema::new(7, 0.7).unwrap();
206        let mut b = GeneralizedDema::new(7, 0.7).unwrap();
207        assert_eq!(
208            a.batch(&prices),
209            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
210        );
211    }
212
213    #[test]
214    fn reset_clears_state() {
215        let mut gd = GeneralizedDema::new(5, 0.7).unwrap();
216        gd.batch(&(1..=50).map(f64::from).collect::<Vec<_>>());
217        assert!(gd.is_ready());
218        gd.reset();
219        assert!(!gd.is_ready());
220        assert_eq!(gd.update(1.0), None);
221    }
222}