Skip to main content

quantwave_core/indicators/
tema.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5/// Triple Exponential Moving Average (TEMA)
6/// TEMA = (3 * EMA1) - (3 * EMA2) + EMA3
7/// where EMA1 = EMA(Close), EMA2 = EMA(EMA1), EMA3 = EMA(EMA2)
8#[derive(Debug, Clone)]
9pub struct TEMA {
10    ema1: EMA,
11    ema2: EMA,
12    ema3: EMA,
13}
14
15impl TEMA {
16    pub fn new(period: usize) -> Self {
17        Self {
18            ema1: EMA::new(period),
19            ema2: EMA::new(period),
20            ema3: EMA::new(period),
21        }
22    }
23}
24
25impl Next<f64> for TEMA {
26    type Output = f64;
27
28    fn next(&mut self, input: f64) -> Self::Output {
29        let e1 = self.ema1.next(input);
30        let e2 = self.ema2.next(e1);
31        let e3 = self.ema3.next(e2);
32
33        3.0 * e1 - 3.0 * e2 + e3
34    }
35}
36
37/// Zero-Lag Exponential Moving Average (ZLEMA)
38/// Sometimes referred to as DEMA or a variation.
39/// ZLEMA = (2 * EMA1) - EMA2
40#[derive(Debug, Clone)]
41pub struct ZLEMA {
42    ema1: EMA,
43    ema2: EMA,
44}
45
46impl ZLEMA {
47    pub fn new(period: usize) -> Self {
48        Self {
49            ema1: EMA::new(period),
50            ema2: EMA::new(period),
51        }
52    }
53}
54
55impl Next<f64> for ZLEMA {
56    type Output = f64;
57
58    fn next(&mut self, input: f64) -> Self::Output {
59        let e1 = self.ema1.next(input);
60        let e2 = self.ema2.next(e1);
61
62        2.0 * e1 - e2
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use proptest::prelude::*;
70    use serde::Deserialize;
71    use std::fs;
72    use std::path::Path;
73
74    #[derive(Debug, Deserialize)]
75    struct TemaCase {
76        close: Vec<f64>,
77        expected_tema: Vec<f64>,
78        expected_zlema: Vec<f64>,
79    }
80
81    #[test]
82    fn test_tema_zlema_gold_standard() {
83        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
84        let manifest_path = Path::new(&manifest_dir);
85        let path = manifest_path.join("tests/gold_standard/tema_14.json");
86        let path = if path.exists() {
87            path
88        } else {
89            manifest_path
90                .parent()
91                .unwrap()
92                .join("tests/gold_standard/tema_14.json")
93        };
94        let content = fs::read_to_string(path).unwrap();
95        let case: TemaCase = serde_json::from_str(&content).unwrap();
96
97        let mut tema = TEMA::new(14);
98        let mut zlema = ZLEMA::new(14);
99
100        for i in 0..case.close.len() {
101            let t = tema.next(case.close[i]);
102            let z = zlema.next(case.close[i]);
103            approx::assert_relative_eq!(t, case.expected_tema[i], epsilon = 1e-6);
104            approx::assert_relative_eq!(z, case.expected_zlema[i], epsilon = 1e-6);
105        }
106    }
107
108    fn tema_batch(data: Vec<f64>, period: usize) -> Vec<f64> {
109        let mut tema = TEMA::new(period);
110        data.into_iter().map(|x| tema.next(x)).collect()
111    }
112
113    fn zlema_batch(data: Vec<f64>, period: usize) -> Vec<f64> {
114        let mut zlema = ZLEMA::new(period);
115        data.into_iter().map(|x| zlema.next(x)).collect()
116    }
117
118    proptest! {
119        #[test]
120        fn test_tema_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
121            let period = 14;
122            let mut tema = TEMA::new(period);
123            let mut streaming_results = Vec::with_capacity(input.len());
124            for &val in &input {
125                streaming_results.push(tema.next(val));
126            }
127
128            let batch_results = tema_batch(input, period);
129
130            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
131                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
132            }
133        }
134
135        #[test]
136        fn test_zlema_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
137            let period = 14;
138            let mut zlema = ZLEMA::new(period);
139            let mut streaming_results = Vec::with_capacity(input.len());
140            for &val in &input {
141                streaming_results.push(zlema.next(val));
142            }
143
144            let batch_results = zlema_batch(input, period);
145
146            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
147                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
148            }
149        }
150    }
151}
152
153pub const TEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
154    name: "Triple Exponential Moving Average",
155    description: "TEMA reduces the lag of traditional EMAs.",
156    usage: "Use to reduce the lag of a standard EMA by approximately two thirds. Drop-in replacement for EMA in trend-following systems where responsiveness is more important than smoothness.",
157    keywords: &["moving-average", "low-lag", "ema", "smoothing", "classic"],
158    ehlers_summary: "Patrick Mulloy introduced Triple EMA in Technical Analysis of Stocks and Commodities (1994) as a practical lag-reduction technique. TEMA = 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA)), subtracting out two orders of the EMA lag while preserving most of the noise reduction.",
159    params: &[ParamDef {
160        name: "period",
161        default: "14",
162        description: "Smoothing period",
163    }],
164    formula_source: "https://www.investopedia.com/terms/t/triple-exponential-moving-average.asp",
165    formula_latex: r#"
166\[
167TEMA = (3 \times EMA_1) - (3 \times EMA_2) + EMA_3
168\]
169"#,
170    gold_standard_file: "tema.json",
171    category: "Classic",
172};
173
174pub const ZLEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
175    name: "Zero Lag Exponential Moving Average",
176    description: "ZLEMA attempts to eliminate the inherent lag associated with moving averages.",
177    usage: "Use to reduce the lag of a standard EMA by approximately two thirds. Drop-in replacement for EMA in trend-following systems where responsiveness is more important than smoothness.",
178    keywords: &["moving-average", "low-lag", "ema", "smoothing", "classic"],
179    ehlers_summary: "Patrick Mulloy introduced Triple EMA in Technical Analysis of Stocks and Commodities (1994) as a practical lag-reduction technique. TEMA = 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA)), subtracting out two orders of the EMA lag while preserving most of the noise reduction.",
180    params: &[ParamDef {
181        name: "period",
182        default: "14",
183        description: "Smoothing period",
184    }],
185    formula_source: "https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average",
186    formula_latex: r#"
187\[
188ZLEMA = EMA(Price + (Price - Price_{t - (period - 1)/2}))
189\]
190"#,
191    gold_standard_file: "zlema.json",
192    category: "Classic",
193};