Skip to main content

quantwave_core/indicators/
adaptive_ema.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Adaptive Exponential Moving Average (AEMA)
6/// TASC April 2019, by Vitali Apirine
7#[derive(Debug, Clone)]
8pub struct AdaptiveEMA {
9    _period: usize,
10    pds: usize,
11    mltp1: f64,
12    highs: VecDeque<f64>,
13    lows: VecDeque<f64>,
14    prev_aema: Option<f64>,
15}
16
17impl AdaptiveEMA {
18    pub fn new(period: usize, pds: usize) -> Self {
19        Self {
20            _period: period,
21            pds,
22            mltp1: 2.0 / (period as f64 + 1.0),
23            highs: VecDeque::with_capacity(pds),
24            lows: VecDeque::with_capacity(pds),
25            prev_aema: None,
26        }
27    }
28}
29
30impl Next<(f64, f64, f64)> for AdaptiveEMA {
31    type Output = f64;
32
33    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
34        self.highs.push_back(high);
35        self.lows.push_back(low);
36
37        if self.highs.len() > self.pds {
38            self.highs.pop_front();
39            self.lows.pop_front();
40        }
41
42        let mut max_high = f64::MIN;
43        for &h in self.highs.iter() {
44            if h > max_high {
45                max_high = h;
46            }
47        }
48
49        let mut min_low = f64::MAX;
50        for &l in self.lows.iter() {
51            if l < min_low {
52                min_low = l;
53            }
54        }
55
56        let mltp2 = if max_high - min_low == 0.0 {
57            0.0
58        } else {
59            (((close - min_low) - (max_high - close)).abs()) / (max_high - min_low)
60        };
61
62        let rate = self.mltp1 * (1.0 + mltp2);
63
64        let aema = match self.prev_aema {
65            Some(prev) => prev + rate * (close - prev),
66            None => close,
67        };
68
69        self.prev_aema = Some(aema);
70        aema
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use proptest::prelude::*;
78
79    fn adaptive_ema_batch(data: Vec<(f64, f64, f64)>, period: usize, pds: usize) -> Vec<f64> {
80        let mut aema = AdaptiveEMA::new(period, pds);
81        data.into_iter().map(|x| aema.next(x)).collect()
82    }
83
84    proptest! {
85        #[test]
86        fn test_adaptive_ema_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
87            let mut adj_input = Vec::with_capacity(input.len());
88            for (h, l, c) in input {
89                let h_f: f64 = h;
90                let l_f: f64 = l;
91                let c_f: f64 = c;
92                let high = h_f.max(l_f).max(c_f);
93                let low = l_f.min(h_f).min(c_f);
94                adj_input.push((high, low, c_f));
95            }
96
97            let period = 10;
98            let pds = 10;
99            let mut aema = AdaptiveEMA::new(period, pds);
100            let mut streaming_results = Vec::with_capacity(adj_input.len());
101            for &val in &adj_input {
102                streaming_results.push(aema.next(val));
103            }
104
105            let batch_results = adaptive_ema_batch(adj_input, period, pds);
106
107            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
108                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
109            }
110        }
111    }
112
113    #[test]
114    fn test_adaptive_ema_basic() {
115        let mut aema = AdaptiveEMA::new(10, 10);
116        let val1 = aema.next((10.0, 8.0, 9.0));
117        assert_eq!(val1, 9.0); // Starts with close
118
119        let val2 = aema.next((12.0, 7.0, 11.0));
120        assert!(val2 > 9.0); // Should move up
121    }
122}
123
124pub const ADAPTIVE_EMA_METADATA: IndicatorMetadata = IndicatorMetadata {
125    name: "Adaptive Exponential Moving Average",
126    description: "An adaptive moving average that adjusts its smoothing factor based on volatility.",
127    usage: "Use to identify overall trends. AEMA reacts faster to large price movements by adapting the smoothing factor using the highest high and lowest low of a lookback period.",
128    keywords: &["moving-average", "adaptive", "volatility", "trend"],
129    ehlers_summary: "Introduced by Vitali Apirine in TASC April 2019, AEMA alters the EMA's alpha (smoothing factor) by comparing the distance of the close from the lowest low and highest high. This amplifies the smoothing factor during strong price moves while reducing it during sideways chop, yielding a moving average with less lag when it matters most.",
130    params: &[
131        ParamDef {
132            name: "period",
133            default: "10",
134            description: "Smoothing period",
135        },
136        ParamDef {
137            name: "pds",
138            default: "10",
139            description: "Lookback period for volatility",
140        },
141    ],
142    formula_source: "Technical Analysis of Stocks & Commodities, April 2019",
143    formula_latex: r#"
144\[
145Rate = \frac{2}{P+1} \times \left(1 + \frac{|(C - L_{min}) - (H_{max} - C)|}{H_{max} - L_{min}}\right) \\ AEMA_t = AEMA_{t-1} + Rate \times (C - AEMA_{t-1})
146\]
147"#,
148    gold_standard_file: "",
149    category: "Moving Averages",
150};