quantwave_core/indicators/
exp_dev_bands.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{EMA, SMA};
3use crate::traits::Next;
4use serde::{Deserialize, Serialize};
5
6pub const METADATA: IndicatorMetadata = IndicatorMetadata {
7 name: "Exponential Deviation Bands",
8 description: "A price band indicator based on exponential deviation that applies more weight to recent data and generates fewer breakouts than standard deviation bands.",
9 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.",
10 keywords: &["bands", "volatility", "exponential-deviation", "trend"],
11 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.",
12 params: &[
13 ParamDef {
14 name: "period",
15 default: "20",
16 description: "Period for the base moving average and exponential deviation.",
17 },
18 ParamDef {
19 name: "dev_mult",
20 default: "2.0",
21 description: "Multiplier for the exponential deviation.",
22 },
23 ParamDef {
24 name: "baseline",
25 default: "ema",
26 description: "Base moving average: ExpDevBaseline::Ema or ExpDevBaseline::Sma.",
27 },
28 ],
29 formula_source: "Technical Analysis of Stocks & Commodities, July 2019",
30 formula_latex: r#"
31\[
32BaseMA = \text{SMA or EMA}(Price, n) \\
33Deviation = |BaseMA - Price| \\
34ExpDev = EMA(Deviation, n) \\
35Upper = BaseMA + ExpDev \times multiplier \\
36Lower = BaseMA - ExpDev \times multiplier
37\]
38"#,
39 gold_standard_file: "exp_dev_bands_20_2.json",
40 category: "Classic",
41};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
45pub enum ExpDevBaseline {
46 #[default]
47 Ema,
48 Sma,
49}
50
51impl ExpDevBaseline {
52 pub const fn is_sma(self) -> bool {
53 matches!(self, Self::Sma)
54 }
55
56 pub const fn is_ema(self) -> bool {
57 matches!(self, Self::Ema)
58 }
59}
60
61impl From<bool> for ExpDevBaseline {
62 fn from(use_sma: bool) -> Self {
63 if use_sma { Self::Sma } else { Self::Ema }
64 }
65}
66
67impl From<ExpDevBaseline> for bool {
68 fn from(b: ExpDevBaseline) -> Self {
69 b.is_sma()
70 }
71}
72
73#[derive(Debug, Clone)]
74enum BaseMA {
75 SMA(SMA),
76 EMA(EMA),
77}
78
79impl Next<f64> for BaseMA {
80 type Output = f64;
81 fn next(&mut self, input: f64) -> Self::Output {
82 match self {
83 BaseMA::SMA(inner) => inner.next(input),
84 BaseMA::EMA(inner) => inner.next(input),
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct ExpDevBands {
92 base_ma: BaseMA,
93 exp_dev_ema: EMA,
94 multiplier: f64,
95}
96
97impl ExpDevBands {
98 pub fn new(period: usize, multiplier: f64, baseline: ExpDevBaseline) -> Self {
99 let base_ma = match baseline {
100 ExpDevBaseline::Sma => BaseMA::SMA(SMA::new(period)),
101 ExpDevBaseline::Ema => BaseMA::EMA(EMA::new(period)),
102 };
103
104 Self {
105 base_ma,
106 exp_dev_ema: EMA::new(period),
107 multiplier,
108 }
109 }
110}
111
112impl Next<f64> for ExpDevBands {
113 type Output = (f64, f64, f64); fn next(&mut self, input: f64) -> Self::Output {
116 let basis = self.base_ma.next(input);
117 let deviation = (basis - input).abs();
118 let exp_dev = self.exp_dev_ema.next(deviation);
119
120 let upper = basis + exp_dev * self.multiplier;
121 let lower = basis - exp_dev * self.multiplier;
122
123 (upper, basis, lower)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use proptest::prelude::*;
131
132 #[test]
133 fn test_exp_dev_bands_basic() {
134 let mut edb = ExpDevBands::new(20, 2.0, ExpDevBaseline::Sma);
135 let price = 100.0;
136 let (upper, basis, lower) = edb.next(price);
137
138 approx::assert_relative_eq!(basis, 100.0);
139 approx::assert_relative_eq!(upper, 100.0);
140 approx::assert_relative_eq!(lower, 100.0);
141
142 let (upper, basis, _lower) = edb.next(110.0);
143 assert_eq!(basis, 105.0);
146 approx::assert_relative_eq!(basis, 105.0);
158 approx::assert_relative_eq!(upper, 105.95238, epsilon = 1e-4);
159 }
160
161 fn exp_dev_bands_batch(
162 data: &[f64],
163 period: usize,
164 multiplier: f64,
165 baseline: ExpDevBaseline,
166 ) -> Vec<(f64, f64, f64)> {
167 let mut edb = ExpDevBands::new(period, multiplier, baseline);
168 data.iter().map(|&x| edb.next(x)).collect()
169 }
170
171 proptest! {
172 #[test]
173 fn test_exp_dev_bands_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
174 let period = 20;
175 let multiplier = 2.0;
176 let baseline = ExpDevBaseline::Ema;
177
178 let mut edb = ExpDevBands::new(period, multiplier, baseline);
179 let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| edb.next(x)).collect();
180 let batch_results = exp_dev_bands_batch(&input, period, multiplier, baseline);
181
182 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
183 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
184 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
185 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
186 }
187 }
188 }
189}