use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::indicators::smoothing::EMA;
use crate::traits::Next;
#[derive(Debug, Clone)]
pub struct TEMA {
ema1: EMA,
ema2: EMA,
ema3: EMA,
}
impl TEMA {
pub fn new(period: usize) -> Self {
Self {
ema1: EMA::new(period),
ema2: EMA::new(period),
ema3: EMA::new(period),
}
}
}
impl Next<f64> for TEMA {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
let e1 = self.ema1.next(input);
let e2 = self.ema2.next(e1);
let e3 = self.ema3.next(e2);
3.0 * e1 - 3.0 * e2 + e3
}
}
#[derive(Debug, Clone)]
pub struct ZLEMA {
ema1: EMA,
ema2: EMA,
}
impl ZLEMA {
pub fn new(period: usize) -> Self {
Self {
ema1: EMA::new(period),
ema2: EMA::new(period),
}
}
}
impl Next<f64> for ZLEMA {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
let e1 = self.ema1.next(input);
let e2 = self.ema2.next(e1);
2.0 * e1 - e2
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct TemaCase {
close: Vec<f64>,
expected_tema: Vec<f64>,
expected_zlema: Vec<f64>,
}
#[test]
fn test_tema_zlema_gold_standard() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = Path::new(&manifest_dir);
let path = manifest_path.join("tests/gold_standard/tema_14.json");
let path = if path.exists() {
path
} else {
manifest_path
.parent()
.unwrap()
.join("tests/gold_standard/tema_14.json")
};
let content = fs::read_to_string(path).unwrap();
let case: TemaCase = serde_json::from_str(&content).unwrap();
let mut tema = TEMA::new(14);
let mut zlema = ZLEMA::new(14);
for i in 0..case.close.len() {
let t = tema.next(case.close[i]);
let z = zlema.next(case.close[i]);
approx::assert_relative_eq!(t, case.expected_tema[i], epsilon = 1e-6);
approx::assert_relative_eq!(z, case.expected_zlema[i], epsilon = 1e-6);
}
}
fn tema_batch(data: Vec<f64>, period: usize) -> Vec<f64> {
let mut tema = TEMA::new(period);
data.into_iter().map(|x| tema.next(x)).collect()
}
fn zlema_batch(data: Vec<f64>, period: usize) -> Vec<f64> {
let mut zlema = ZLEMA::new(period);
data.into_iter().map(|x| zlema.next(x)).collect()
}
proptest! {
#[test]
fn test_tema_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
let period = 14;
let mut tema = TEMA::new(period);
let mut streaming_results = Vec::with_capacity(input.len());
for &val in &input {
streaming_results.push(tema.next(val));
}
let batch_results = tema_batch(input, period);
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-6);
}
}
#[test]
fn test_zlema_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
let period = 14;
let mut zlema = ZLEMA::new(period);
let mut streaming_results = Vec::with_capacity(input.len());
for &val in &input {
streaming_results.push(zlema.next(val));
}
let batch_results = zlema_batch(input, period);
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-6);
}
}
}
}
pub const TEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Triple Exponential Moving Average",
description: "TEMA reduces the lag of traditional EMAs.",
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.",
keywords: &["moving-average", "low-lag", "ema", "smoothing", "classic"],
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.",
params: &[ParamDef {
name: "period",
default: "14",
description: "Smoothing period",
}],
formula_source: "https://www.investopedia.com/terms/t/triple-exponential-moving-average.asp",
formula_latex: r#"
\[
TEMA = (3 \times EMA_1) - (3 \times EMA_2) + EMA_3
\]
"#,
gold_standard_file: "tema.json",
category: "Classic",
};
pub const ZLEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Zero Lag Exponential Moving Average",
description: "ZLEMA attempts to eliminate the inherent lag associated with moving averages.",
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.",
keywords: &["moving-average", "low-lag", "ema", "smoothing", "classic"],
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.",
params: &[ParamDef {
name: "period",
default: "14",
description: "Smoothing period",
}],
formula_source: "https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average",
formula_latex: r#"
\[
ZLEMA = EMA(Price + (Price - Price_{t - (period - 1)/2}))
\]
"#,
gold_standard_file: "zlema.json",
category: "Classic",
};