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;
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/// Base moving average algorithm for Exponential Deviation Bands.
44#[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/// Exponential Deviation Bands
90#[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); // (Upper, Basis, Lower)
114
115    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        // SMA(2) of [100, 110] = 105.0
144        // SMA(20) of [100, 110] = 105.0 (window not full)
145        assert_eq!(basis, 105.0);
146        // Deviation = |105 - 110| = 5.0
147        // EMA of Dev: first input is 0.0?
148        // Wait, SMA(100) next is 100. Dev = |100-100| = 0.
149        // EMA(Dev) next(5.0) where prev was 0.0?
150        // EMA starts with first input.
151        // Turn 1: SMA next(100) -> 100. Dev = 0. EMA next(0) -> 0. (upper, basis, lower) = (100, 100, 100).
152        // Turn 2: SMA next(110) -> 105. Dev = |105-110|=5. EMA next(5) -> 0.095 * 5 + 0.905 * 0 = 0.476 (if period 20)
153        // Wait, EMA alpha = 2 / (20 + 1) = 2/21 = 0.095238
154        // Next value = 0.095238 * 5.0 + (1 - 0.095238) * 0.0 = 0.47619
155        // Upper = 105 + 2 * 0.47619 = 105.95238
156
157        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}