wickra_core/indicators/
tema.rs1use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
25pub struct Tema {
26 ema1: Ema,
27 ema2: Ema,
28 ema3: Ema,
29 period: usize,
30}
31
32impl Tema {
33 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 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 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 #[test]
159 fn accessors_and_metadata() {
160 let tema = Tema::new(5).unwrap();
161 assert_eq!(tema.period(), 5);
162 assert_eq!(tema.warmup_period(), 3 * 5 - 2);
164 assert_eq!(tema.name(), "TEMA");
165 }
166}