Skip to main content

kestrel_chartkit/indicator/
tema.rs

1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4
5/// Triple Exponential Moving Average (TEMA) over the closing price.
6///
7/// `TEMA = 3 * e1 - 3 * e2 + e3` over three chained exponential averages of `period` — `e1` over
8/// the close, `e2` over `e1`, `e3` over `e2` — each the shared [`Ema`] with its first-sample
9/// seed. All three run from the first close; only the output is withheld until the `period`-th
10/// bar.
11///
12/// That chaining differs from [`super::moving_averages::DemaEngine`], whose second stage starts
13/// with the first *published* value of the first. The two produce different early values and
14/// converge as the seeds decay.
15///
16/// First output: with the `period`-th bar. [`Indicator::reset`] clears all three averages.
17#[derive(Debug, Clone)]
18pub struct TemaEngine {
19    period: usize,
20    ema1: Ema,
21    ema2: Ema,
22    ema3: Ema,
23    count: usize,
24}
25
26impl TemaEngine {
27    pub fn new(period: usize) -> Self {
28        Self {
29            period: period.max(1),
30            ema1: Ema::new(period),
31            ema2: Ema::new(period),
32            ema3: Ema::new(period),
33            count: 0,
34        }
35    }
36}
37
38impl Indicator for TemaEngine {
39    fn name(&self) -> &str {
40        "tema"
41    }
42
43    fn warmup_period(&self) -> usize {
44        self.period * 3
45    }
46
47    fn reset(&mut self) {
48        self.ema1.reset();
49        self.ema2.reset();
50        self.ema3.reset();
51        self.count = 0;
52    }
53
54    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
55        self.count += 1;
56        let e1 = self.ema1.update(bar.close)?;
57        let e2 = self.ema2.update(e1)?;
58        let e3 = self.ema3.update(e2)?;
59
60        if self.count < self.period {
61            return None;
62        }
63
64        let tema_val = 3.0 * e1 - 3.0 * e2 + e3;
65        Some(IndicatorOutput::new(tema_val))
66    }
67
68    fn alerts(&self) -> Vec<IndicatorAlert> {
69        Vec::new()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_tema_basic() {
79        let mut tema = TemaEngine::new(5);
80        let mut out = None;
81        for i in 0..20 {
82            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
83            out = tema.on_bar(&b);
84        }
85        assert!(out.is_some());
86    }
87}