Skip to main content

quantwave_core/indicators/
tradj_ema.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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) => {
38                if pc > high {
39                    pc
40                } else {
41                    high
42                }
43            }
44            None => high,
45        };
46        let tl = match self.prev_close {
47            Some(pc) => {
48                if pc < low {
49                    pc
50                } else {
51                    low
52                }
53            }
54            None => low,
55        };
56        self.prev_close = Some(close);
57
58        let tr = (th - tl).abs();
59        self.trs.push_back(tr);
60
61        if self.trs.len() > self.pds {
62            self.trs.pop_front();
63        }
64
65        let mut max_tr = f64::MIN;
66        let mut min_tr = f64::MAX;
67        for &t in self.trs.iter() {
68            if t > max_tr {
69                max_tr = t;
70            }
71            if t < min_tr {
72                min_tr = t;
73            }
74        }
75
76        let tradj = if max_tr - min_tr == 0.0 {
77            0.0
78        } else {
79            (tr - min_tr) / (max_tr - min_tr)
80        };
81
82        let mltp2 = tradj * self.mltp;
83        let rate = self.mltp1 * (1.0 + mltp2);
84
85        let ema = match self.prev_ema {
86            Some(prev) => prev + rate * (close - prev),
87            None => close, // Initial value is close according to TradeStation reference
88        };
89
90        self.prev_ema = Some(ema);
91        ema
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use proptest::prelude::*;
99
100    fn tradj_ema_batch(
101        data: Vec<(f64, f64, f64)>,
102        period: usize,
103        pds: usize,
104        mltp: f64,
105    ) -> Vec<f64> {
106        let mut ema = TRAdjEMA::new(period, pds, mltp);
107        data.into_iter().map(|x| ema.next(x)).collect()
108    }
109
110    proptest! {
111        #[test]
112        fn test_tradj_ema_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
113            let mut adj_input = Vec::with_capacity(input.len());
114            for (h, l, c) in input {
115                let h_f: f64 = h;
116                let l_f: f64 = l;
117                let c_f: f64 = c;
118                let high = h_f.max(l_f).max(c_f);
119                let low = l_f.min(h_f).min(c_f);
120                adj_input.push((high, low, c_f));
121            }
122
123            let period = 40;
124            let pds = 40;
125            let mltp = 10.0;
126            let mut ema = TRAdjEMA::new(period, pds, mltp);
127            let mut streaming_results = Vec::with_capacity(adj_input.len());
128            for &val in &adj_input {
129                streaming_results.push(ema.next(val));
130            }
131
132            let batch_results = tradj_ema_batch(adj_input, period, pds, mltp);
133
134            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
135                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
136            }
137        }
138    }
139
140    #[test]
141    fn test_tradj_ema_basic() {
142        let mut ema = TRAdjEMA::new(10, 10, 5.0);
143        let val1 = ema.next((10.0, 8.0, 9.0));
144        assert_eq!(val1, 9.0); // Starts with close
145
146        let val2 = ema.next((12.0, 7.0, 11.0));
147        assert!(val2 > 9.0); // Should move up
148    }
149}
150
151pub const TRADJ_EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
152    name: "True Range Adjusted Exponential Moving Average",
153    description: "An exponential moving average that incorporates true range to measure volatility and adapt to price movements.",
154    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.",
155    keywords: &["moving-average", "adaptive", "true-range", "volatility"],
156    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.",
157    params: &[
158        ParamDef {
159            name: "period",
160            default: "40",
161            description: "Smoothing period",
162        },
163        ParamDef {
164            name: "pds",
165            default: "40",
166            description: "Lookback period for True Range",
167        },
168        ParamDef {
169            name: "mltp",
170            default: "10.0",
171            description: "Multiplier",
172        },
173    ],
174    formula_source: "Technical Analysis of Stocks & Commodities, January 2023",
175    formula_latex: r#"
176\[
177TRAdj = \frac{TR - TR_{min}}{TR_{max} - TR_{min}} \\ Rate = \frac{2}{P+1} \times (1 + TRAdj \times Multiplier)
178\]
179"#,
180    gold_standard_file: "",
181    category: "Moving Averages",
182};