quantwave_core/indicators/
tradj_ema.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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) => 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, };
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); let val2 = ema.next((12.0, 7.0, 11.0));
130 assert!(val2 > 9.0); }
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};