quantwave_core/indicators/
sve_volatility_bands.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{SMA, WMA};
3use crate::traits::Next;
4
5pub const METADATA: IndicatorMetadata = IndicatorMetadata {
6 name: "SVE Volatility Bands",
7 description: "Volatility bands designed to highlight volatility changes especially when using non-time-related charts like Renko.",
8 usage: "Use to identify extreme price excursions and volatility contraction/expansion. The bands adapt to volatility using a smoothed ATR-like calculation.",
9 keywords: &["bands", "volatility", "renko", "vervoort"],
10 ehlers_summary: "Introduced by Sylvain Vervoort, SVE Volatility Bands use a weighted moving average of price and a smoothed True Range to create dynamic bands. It includes a specific adjustment for the lower band and a midline based on typical price.",
11 params: &[
12 ParamDef {
13 name: "bands_period",
14 default: "20",
15 description: "Period for the price WMA and the ATR smoothing basis.",
16 },
17 ParamDef {
18 name: "bands_deviation",
19 default: "2.4",
20 description: "Multiplier for the volatility range.",
21 },
22 ParamDef {
23 name: "low_band_adjust",
24 default: "0.9",
25 description: "Adjustment factor for the lower band.",
26 },
27 ParamDef {
28 name: "mid_line_length",
29 default: "20",
30 description: "Period for the midline WMA.",
31 },
32 ],
33 formula_source: "Technical Analysis of Stocks & Commodities, January 2019",
34 formula_latex: r#"
35\[
36ATR\_MA = SMA(TrueRange, bands\_period \times 2 - 1) \\
37WtdAvgVal = WMA(Close, bands\_period) \\
38Upper = WtdAvgVal \times (1 + (ATR\_MA \times bands\_deviation) / Close) \\
39Lower = WtdAvgVal \times (1 - (ATR\_MA \times bands\_deviation \times low\_band\_adjust) / Close) \\
40MidLine = WMA(TypicalPrice, mid\_line\_length)
41\]
42"#,
43 gold_standard_file: "sve_volatility_bands_20_2.4_0.9_20.json",
44 category: "Classic",
45};
46
47#[derive(Debug, Clone)]
49pub struct SVEVolatilityBands {
50 price_wma: WMA,
51 tr_sma: SMA,
52 mid_line_wma: WMA,
53 bands_deviation: f64,
54 low_band_adjust: f64,
55 prev_close: Option<f64>,
56}
57
58impl SVEVolatilityBands {
59 pub fn new(bands_period: usize, bands_deviation: f64, low_band_adjust: f64, mid_line_length: usize) -> Self {
60 Self {
61 price_wma: WMA::new(bands_period),
62 tr_sma: SMA::new(bands_period * 2 - 1),
63 mid_line_wma: WMA::new(mid_line_length),
64 bands_deviation,
65 low_band_adjust,
66 prev_close: None,
67 }
68 }
69}
70
71impl Next<(f64, f64, f64)> for SVEVolatilityBands {
72 type Output = (f64, f64, f64); fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
75 let tr = match self.prev_close {
77 Some(pc) => {
78 let h = high.max(pc);
79 let l = low.min(pc);
80 h - l
81 }
82 None => high - low,
83 };
84 self.prev_close = Some(close);
85
86 let ma_tr = self.tr_sma.next(tr);
87 let wtd_avg_val = self.price_wma.next(close);
88
89 let typical_price = (high + low + close) / 3.0;
90 let mid_line = self.mid_line_wma.next(typical_price);
91
92 let atr_val = ma_tr * self.bands_deviation;
93
94 let upper = wtd_avg_val + wtd_avg_val * (atr_val / close);
99 let lower = wtd_avg_val - wtd_avg_val * (atr_val * self.low_band_adjust / close);
100
101 (upper, mid_line, lower)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use proptest::prelude::*;
109
110 #[test]
111 fn test_sve_volatility_bands_basic() {
112 let mut sve = SVEVolatilityBands::new(20, 2.4, 0.9, 20);
113 let (upper, mid, lower) = sve.next((105.0, 95.0, 100.0));
114
115 assert_eq!(mid, 100.0);
123 assert_eq!(upper, 124.0);
124 assert_eq!(lower, 78.4);
125 }
126
127 fn sve_volatility_bands_batch(
128 data: &[(f64, f64, f64)],
129 period: usize,
130 dev: f64,
131 adj: f64,
132 mid_len: usize,
133 ) -> Vec<(f64, f64, f64)> {
134 let mut sve = SVEVolatilityBands::new(period, dev, adj, mid_len);
135 data.iter().map(|&x| sve.next(x)).collect()
136 }
137
138 proptest! {
139 #[test]
140 fn test_sve_volatility_bands_parity(input in prop::collection::vec((0.1..100.0, 0.1..100.0, 0.1..100.0), 1..100)) {
141 let mut adj_input = Vec::with_capacity(input.len());
142 for (h, l, c) in input {
143 let h_f: f64 = h;
144 let l_f: f64 = l;
145 let c_f: f64 = c;
146 let high = h_f.max(l_f).max(c_f);
147 let low = l_f.min(h_f).min(c_f);
148 adj_input.push((high, low, c_f));
149 }
150
151 let period = 20;
152 let dev = 2.4;
153 let adj = 0.9;
154 let mid_len = 20;
155
156 let mut sve = SVEVolatilityBands::new(period, dev, adj, mid_len);
157 let streaming_results: Vec<(f64, f64, f64)> = adj_input.iter().map(|&x| sve.next(x)).collect();
158 let batch_results = sve_volatility_bands_batch(&adj_input, period, dev, adj, mid_len);
159
160 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
161 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
162 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
163 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
164 }
165 }
166 }
167}