quantwave_core/indicators/
tradj_ema.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5#[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, };
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); let val2 = ema.next((12.0, 7.0, 11.0));
147 assert!(val2 > 9.0); }
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};