Skip to main content

quantwave_core/indicators/
tradj_ema.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// True Range Adjusted Exponential Moving Average (TRAdj EMA)
6/// TASC January 2023, by Vitali Apirine
7#[derive(Debug, Clone)]
8pub struct TRAdjEMA {
9    _period: usize,
10    pds: usize,
11    mltp: f64,
12    mltp1: f64,
13    prev_close: Option<f64>,
14    trs: VecDeque<f64>,
15    prev_ema: Option<f64>,
16}
17
18impl TRAdjEMA {
19    pub fn new(period: usize, pds: usize, mltp: f64) -> Self {
20        Self {
21            _period: period,
22            pds,
23            mltp,
24            mltp1: 2.0 / (period as f64 + 1.0),
25            prev_close: None,
26            trs: VecDeque::with_capacity(pds),
27            prev_ema: None,
28        }
29    }
30}
31
32impl Next<(f64, f64, f64)> for TRAdjEMA {
33    type Output = f64;
34
35    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
36        let th = match self.prev_close {
37            Some(pc) => if pc > high { pc } else { high },
38            None => high,
39        };
40        let tl = match self.prev_close {
41            Some(pc) => if pc < low { pc } else { low },
42            None => low,
43        };
44        self.prev_close = Some(close);
45
46        let tr = (th - tl).abs();
47        self.trs.push_back(tr);
48
49        if self.trs.len() > self.pds {
50            self.trs.pop_front();
51        }
52
53        let mut max_tr = f64::MIN;
54        let mut min_tr = f64::MAX;
55        for &t in self.trs.iter() {
56            if t > max_tr {
57                max_tr = t;
58            }
59            if t < min_tr {
60                min_tr = t;
61            }
62        }
63
64        let tradj = if max_tr - min_tr == 0.0 {
65            0.0
66        } else {
67            (tr - min_tr) / (max_tr - min_tr)
68        };
69
70        let mltp2 = tradj * self.mltp;
71        let rate = self.mltp1 * (1.0 + mltp2);
72
73        let ema = match self.prev_ema {
74            Some(prev) => prev + rate * (close - prev),
75            None => close, // Initial value is close according to TradeStation reference
76        };
77
78        self.prev_ema = Some(ema);
79        ema
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use proptest::prelude::*;
87
88    fn tradj_ema_batch(data: Vec<(f64, f64, f64)>, period: usize, pds: usize, mltp: f64) -> Vec<f64> {
89        let mut ema = TRAdjEMA::new(period, pds, mltp);
90        data.into_iter().map(|x| ema.next(x)).collect()
91    }
92
93    proptest! {
94        #[test]
95        fn test_tradj_ema_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
96            let mut adj_input = Vec::with_capacity(input.len());
97            for (h, l, c) in input {
98                let h_f: f64 = h;
99                let l_f: f64 = l;
100                let c_f: f64 = c;
101                let high = h_f.max(l_f).max(c_f);
102                let low = l_f.min(h_f).min(c_f);
103                adj_input.push((high, low, c_f));
104            }
105
106            let period = 40;
107            let pds = 40;
108            let mltp = 10.0;
109            let mut ema = TRAdjEMA::new(period, pds, mltp);
110            let mut streaming_results = Vec::with_capacity(adj_input.len());
111            for &val in &adj_input {
112                streaming_results.push(ema.next(val));
113            }
114
115            let batch_results = tradj_ema_batch(adj_input, period, pds, mltp);
116
117            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
118                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
119            }
120        }
121    }
122
123    #[test]
124    fn test_tradj_ema_basic() {
125        let mut ema = TRAdjEMA::new(10, 10, 5.0);
126        let val1 = ema.next((10.0, 8.0, 9.0));
127        assert_eq!(val1, 9.0); // Starts with close
128
129        let val2 = ema.next((12.0, 7.0, 11.0));
130        assert!(val2 > 9.0); // Should move up
131    }
132}
133
134pub const TRADJ_EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
135    name: "True Range Adjusted Exponential Moving Average",
136    description: "An exponential moving average that incorporates true range to measure volatility and adapt to price movements.",
137    usage: "Use to identify trend turning points and filter price movements. Comparing TRAdj EMA with a standard EMA of the same length provides insights into the overall trend.",
138    keywords: &["moving-average", "adaptive", "true-range", "volatility"],
139    ehlers_summary: "Introduced by Vitali Apirine in TASC January 2023, TRAdj EMA modifies the standard exponential moving average by adjusting the smoothing factor using the True Range. The normalized true range modifies the rate, making the indicator more responsive during volatile periods while filtering out noise when volatility drops.",
140    params: &[
141        ParamDef {
142            name: "period",
143            default: "40",
144            description: "Smoothing period",
145        },
146        ParamDef {
147            name: "pds",
148            default: "40",
149            description: "Lookback period for True Range",
150        },
151        ParamDef {
152            name: "mltp",
153            default: "10.0",
154            description: "Multiplier",
155        },
156    ],
157    formula_source: "Technical Analysis of Stocks & Commodities, January 2023",
158    formula_latex: r#"
159\[
160TRAdj = \frac{TR - TR_{min}}{TR_{max} - TR_{min}} \\ Rate = \frac{2}{P+1} \times (1 + TRAdj \times Multiplier)
161\]
162"#,
163    gold_standard_file: "",
164    category: "Moving Averages",
165};