Skip to main content

wickra_core/indicators/
tema.rs

1//! Triple Exponential Moving Average (TEMA).
2
3use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7/// Triple Exponential Moving Average: `3 * EMA1 - 3 * EMA2 + EMA3`,
8/// where `EMA2 = EMA(EMA1)` and `EMA3 = EMA(EMA2)`.
9///
10/// Reduces lag further than DEMA at the cost of more responsiveness to noise.
11///
12/// # Example
13///
14/// ```
15/// use wickra_core::{Indicator, Tema};
16///
17/// let mut indicator = Tema::new(3).unwrap();
18/// let mut last = None;
19/// for i in 0..80 {
20///     last = indicator.update(100.0 + f64::from(i));
21/// }
22/// assert!(last.is_some());
23/// ```
24#[derive(Debug, Clone)]
25pub struct Tema {
26    ema1: Ema,
27    ema2: Ema,
28    ema3: Ema,
29    period: usize,
30}
31
32impl Tema {
33    /// # Errors
34    /// Returns [`crate::Error::PeriodZero`] if `period == 0`.
35    pub fn new(period: usize) -> Result<Self> {
36        Ok(Self {
37            ema1: Ema::new(period)?,
38            ema2: Ema::new(period)?,
39            ema3: Ema::new(period)?,
40            period,
41        })
42    }
43
44    /// Configured period.
45    pub const fn period(&self) -> usize {
46        self.period
47    }
48}
49
50impl Indicator for Tema {
51    type Input = f64;
52    type Output = f64;
53
54    #[inline]
55    fn update(&mut self, input: f64) -> Option<f64> {
56        let e1 = self.ema1.update(input)?;
57        let e2 = self.ema2.update(e1)?;
58        let e3 = self.ema3.update(e2)?;
59        Some(3.0 * e1 - 3.0 * e2 + e3)
60    }
61
62    fn reset(&mut self) {
63        self.ema1.reset();
64        self.ema2.reset();
65        self.ema3.reset();
66    }
67
68    #[inline]
69    fn warmup_period(&self) -> usize {
70        3 * self.period - 2
71    }
72
73    #[inline]
74    fn is_ready(&self) -> bool {
75        self.ema3.is_ready()
76    }
77
78    #[inline]
79    fn name(&self) -> &'static str {
80        "TEMA"
81    }
82
83    /// SIMD kernel: like DEMA's, with a third chained EMA scan. Agrees with the
84    /// exact batch to within a few units in the last place; warmup `NaN`s and
85    /// length are identical. Afterwards the three EMAs continue streaming from
86    /// the kernel's last values.
87    fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
88        assert_eq!(
89            inputs.len(),
90            out.len(),
91            "batch output length must equal input length"
92        );
93        let p = self.period;
94        if !(self.ema1.is_fresh() && self.ema2.is_fresh() && self.ema3.is_fresh())
95            || inputs.len() < 3 * p - 2
96            || !crate::fast::in_range(inputs)
97        {
98            self.batch_nan_into(inputs, out);
99            return;
100        }
101        let (e1, e2, e3) = wickra_simd::dispatch(crate::fast::TemaFast {
102            x: inputs,
103            period: p,
104            alpha: self.ema1.alpha(),
105            out,
106            _borrow: std::marker::PhantomData,
107        });
108        self.ema1.seed_to(e1);
109        self.ema2.seed_to(e2);
110        self.ema3.seed_to(e3);
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::traits::BatchExt;
118    use approx::assert_relative_eq;
119
120    #[test]
121    fn constant_series_yields_constant_tema() {
122        let mut tema = Tema::new(5).unwrap();
123        let out = tema.batch(&[42.0_f64; 80]);
124        let last = out.iter().rev().flatten().next().unwrap();
125        assert_relative_eq!(*last, 42.0, epsilon = 1e-9);
126    }
127
128    #[test]
129    fn batch_equals_streaming() {
130        let prices: Vec<f64> = (1..=80)
131            .map(|i| (f64::from(i) * 0.3).sin() * 10.0)
132            .collect();
133        let mut a = Tema::new(5).unwrap();
134        let mut b = Tema::new(5).unwrap();
135        assert_eq!(
136            a.batch(&prices),
137            prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
138        );
139    }
140
141    #[test]
142    fn reset_clears_state() {
143        let mut tema = Tema::new(5).unwrap();
144        tema.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
145        assert!(tema.is_ready());
146        tema.reset();
147        assert!(!tema.is_ready());
148    }
149
150    #[test]
151    fn rejects_zero_period() {
152        assert!(Tema::new(0).is_err());
153    }
154
155    /// Cover the const accessor `period` (45-47) and the Indicator-impl
156    /// `warmup_period` (67-69) + `name` (75-77). Existing tests inspect
157    /// TEMA output but never query the metadata.
158    #[test]
159    fn accessors_and_metadata() {
160        let tema = Tema::new(5).unwrap();
161        assert_eq!(tema.period(), 5);
162        // EMA1 seeds at period (5), each cascade stage needs another (period-1) inputs.
163        assert_eq!(tema.warmup_period(), 3 * 5 - 2);
164        assert_eq!(tema.name(), "TEMA");
165    }
166}