Skip to main content

quantwave_core/indicators/
exp_dev_bands.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{EMA, SMA};
3use crate::traits::Next;
4
5pub const METADATA: IndicatorMetadata = IndicatorMetadata {
6    name: "Exponential Deviation Bands",
7    description: "A price band indicator based on exponential deviation that applies more weight to recent data and generates fewer breakouts than standard deviation bands.",
8    usage: "Use as a tool to identify trends and potential trend reversals. Prices consistently above the upper band indicate a strong uptrend, while prices below the lower band indicate a strong downtrend.",
9    keywords: &["bands", "volatility", "exponential-deviation", "trend"],
10    ehlers_summary: "Introduced by Vitali Apirine, Exponential Deviation Bands use an EMA of the absolute deviation from a base moving average (SMA or EMA) to create volatility bands. This approach is more responsive to recent price changes than standard deviation-based Bollinger Bands.",
11    params: &[
12        ParamDef {
13            name: "period",
14            default: "20",
15            description: "Period for the base moving average and exponential deviation.",
16        },
17        ParamDef {
18            name: "dev_mult",
19            default: "2.0",
20            description: "Multiplier for the exponential deviation.",
21        },
22        ParamDef {
23            name: "use_sma",
24            default: "false",
25            description: "Whether to use SMA (true) or EMA (false) as the base moving average.",
26        },
27    ],
28    formula_source: "Technical Analysis of Stocks & Commodities, July 2019",
29    formula_latex: r#"
30\[
31BaseMA = \text{SMA or EMA}(Price, n) \\
32Deviation = |BaseMA - Price| \\
33ExpDev = EMA(Deviation, n) \\
34Upper = BaseMA + ExpDev \times multiplier \\
35Lower = BaseMA - ExpDev \times multiplier
36\]
37"#,
38    gold_standard_file: "exp_dev_bands_20_2.json",
39    category: "Classic",
40};
41
42#[derive(Debug, Clone)]
43enum BaseMA {
44    SMA(SMA),
45    EMA(EMA),
46}
47
48impl Next<f64> for BaseMA {
49    type Output = f64;
50    fn next(&mut self, input: f64) -> Self::Output {
51        match self {
52            BaseMA::SMA(inner) => inner.next(input),
53            BaseMA::EMA(inner) => inner.next(input),
54        }
55    }
56}
57
58/// Exponential Deviation Bands
59#[derive(Debug, Clone)]
60pub struct ExpDevBands {
61    base_ma: BaseMA,
62    exp_dev_ema: EMA,
63    multiplier: f64,
64}
65
66impl ExpDevBands {
67    pub fn new(period: usize, multiplier: f64, use_sma: bool) -> Self {
68        let base_ma = if use_sma {
69            BaseMA::SMA(SMA::new(period))
70        } else {
71            BaseMA::EMA(EMA::new(period))
72        };
73
74        Self {
75            base_ma,
76            exp_dev_ema: EMA::new(period),
77            multiplier,
78        }
79    }
80}
81
82impl Next<f64> for ExpDevBands {
83    type Output = (f64, f64, f64); // (Upper, Basis, Lower)
84
85    fn next(&mut self, input: f64) -> Self::Output {
86        let basis = self.base_ma.next(input);
87        let deviation = (basis - input).abs();
88        let exp_dev = self.exp_dev_ema.next(deviation);
89
90        let upper = basis + exp_dev * self.multiplier;
91        let lower = basis - exp_dev * self.multiplier;
92
93        (upper, basis, lower)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use proptest::prelude::*;
101
102    #[test]
103    fn test_exp_dev_bands_basic() {
104        let mut edb = ExpDevBands::new(20, 2.0, true);
105        let price = 100.0;
106        let (upper, basis, lower) = edb.next(price);
107        
108        approx::assert_relative_eq!(basis, 100.0);
109        approx::assert_relative_eq!(upper, 100.0);
110        approx::assert_relative_eq!(lower, 100.0);
111        
112        let (upper, basis, _lower) = edb.next(110.0);
113        // SMA(2) of [100, 110] = 105.0
114        // SMA(20) of [100, 110] = 105.0 (window not full)
115        assert_eq!(basis, 105.0);
116        // Deviation = |105 - 110| = 5.0
117        // EMA of Dev: first input is 0.0? 
118        // Wait, SMA(100) next is 100. Dev = |100-100| = 0.
119        // EMA(Dev) next(5.0) where prev was 0.0? 
120        // EMA starts with first input.
121        // Turn 1: SMA next(100) -> 100. Dev = 0. EMA next(0) -> 0. (upper, basis, lower) = (100, 100, 100).
122        // Turn 2: SMA next(110) -> 105. Dev = |105-110|=5. EMA next(5) -> 0.095 * 5 + 0.905 * 0 = 0.476 (if period 20)
123        // Wait, EMA alpha = 2 / (20 + 1) = 2/21 = 0.095238
124        // Next value = 0.095238 * 5.0 + (1 - 0.095238) * 0.0 = 0.47619
125        // Upper = 105 + 2 * 0.47619 = 105.95238
126        
127        approx::assert_relative_eq!(basis, 105.0);
128        approx::assert_relative_eq!(upper, 105.95238, epsilon = 1e-4);
129    }
130
131    fn exp_dev_bands_batch(data: &[f64], period: usize, multiplier: f64, use_sma: bool) -> Vec<(f64, f64, f64)> {
132        let mut edb = ExpDevBands::new(period, multiplier, use_sma);
133        data.iter().map(|&x| edb.next(x)).collect()
134    }
135
136    proptest! {
137        #[test]
138        fn test_exp_dev_bands_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
139            let period = 20;
140            let multiplier = 2.0;
141            let use_sma = false;
142            
143            let mut edb = ExpDevBands::new(period, multiplier, use_sma);
144            let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| edb.next(x)).collect();
145            let batch_results = exp_dev_bands_batch(&input, period, multiplier, use_sma);
146
147            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
148                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
149                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
150                approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
151            }
152        }
153    }
154}