Skip to main content

mantis_ta/indicators/trend/
tema.rs

1use super::EMA;
2use crate::indicators::Indicator;
3use crate::types::Candle;
4
5/// Triple Exponential Moving Average over closing prices.
6///
7/// TEMA = 3 * EMA - 3 * EMA(EMA) + EMA(EMA(EMA)), further reducing lag.
8///
9/// # Examples
10/// ```rust
11/// use mantis_ta::indicators::{Indicator, TEMA};
12/// use mantis_ta::types::Candle;
13///
14/// let prices = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
15/// let candles: Vec<Candle> = prices
16///     .iter()
17///     .enumerate()
18///     .map(|(i, p)| Candle {
19///         timestamp: i as i64,
20///         open: *p,
21///         high: *p,
22///         low: *p,
23///         close: *p,
24///         volume: 0.0,
25///     })
26///     .collect();
27///
28/// let out = TEMA::new(3).calculate(&candles);
29/// assert!(out.iter().take(6).all(|v| v.is_none()));
30/// assert!(out[6].is_some());
31/// ```
32#[derive(Debug, Clone)]
33pub struct TEMA {
34    period: usize,
35    ema1: EMA,
36    ema2: EMA,
37    ema3: EMA,
38}
39
40impl TEMA {
41    pub fn new(period: usize) -> Self {
42        assert!(period > 0, "period must be > 0");
43        Self {
44            period,
45            ema1: EMA::new(period),
46            ema2: EMA::new(period),
47            ema3: EMA::new(period),
48        }
49    }
50
51    #[inline]
52    fn update(&mut self, value: f64) -> Option<f64> {
53        let ema1_val = self.ema1.next(&Candle {
54            timestamp: 0,
55            open: value,
56            high: value,
57            low: value,
58            close: value,
59            volume: 0.0,
60        });
61
62        if let Some(ema1) = ema1_val {
63            let ema2_val = self.ema2.next(&Candle {
64                timestamp: 0,
65                open: ema1,
66                high: ema1,
67                low: ema1,
68                close: ema1,
69                volume: 0.0,
70            });
71
72            if let Some(ema2) = ema2_val {
73                let ema3_val = self.ema3.next(&Candle {
74                    timestamp: 0,
75                    open: ema2,
76                    high: ema2,
77                    low: ema2,
78                    close: ema2,
79                    volume: 0.0,
80                });
81
82                if let Some(ema3) = ema3_val {
83                    return Some(3.0 * ema1 - 3.0 * ema2 + ema3);
84                }
85            }
86        }
87
88        None
89    }
90}
91
92impl Indicator for TEMA {
93    type Output = f64;
94
95    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
96        self.update(candle.close)
97    }
98
99    fn reset(&mut self) {
100        self.ema1.reset();
101        self.ema2.reset();
102        self.ema3.reset();
103    }
104
105    fn warmup_period(&self) -> usize {
106        self.period * 3 - 2
107    }
108
109    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
110        Box::new(self.clone())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn tema_emits_after_warmup() {
120        let prices = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
121        let candles: Vec<Candle> = prices
122            .iter()
123            .enumerate()
124            .map(|(i, p)| Candle {
125                timestamp: i as i64,
126                open: *p,
127                high: *p,
128                low: *p,
129                close: *p,
130                volume: 0.0,
131            })
132            .collect();
133
134        let out = TEMA::new(3).calculate(&candles);
135        assert!(out.iter().take(6).all(|v| v.is_none()));
136        assert!(out.iter().skip(6).any(|v| v.is_some()));
137    }
138}