quantwave_core/indicators/
tema.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5#[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#[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 params: &[ParamDef {
157 name: "period",
158 default: "14",
159 description: "Smoothing period",
160 }],
161 formula_source: "https://www.investopedia.com/terms/t/triple-exponential-moving-average.asp",
162 formula_latex: r#"
163\[
164TEMA = (3 \times EMA_1) - (3 \times EMA_2) + EMA_3
165\]
166"#,
167 gold_standard_file: "tema.json",
168 category: "Classic",
169};
170
171pub const ZLEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
172 name: "Zero Lag Exponential Moving Average",
173 description: "ZLEMA attempts to eliminate the inherent lag associated with moving averages.",
174 params: &[ParamDef {
175 name: "period",
176 default: "14",
177 description: "Smoothing period",
178 }],
179 formula_source: "https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average",
180 formula_latex: r#"
181\[
182ZLEMA = EMA(Price + (Price - Price_{t - (period - 1)/2}))
183\]
184"#,
185 gold_standard_file: "zlema.json",
186 category: "Classic",
187};