quantwave_core/indicators/
exp_dev_bands.rs1use 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#[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); 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 assert_eq!(basis, 105.0);
116 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}