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(
60 bands_period: usize,
61 bands_deviation: f64,
62 low_band_adjust: f64,
63 mid_line_length: usize,
64 ) -> Self {
65 Self {
66 price_wma: WMA::new(bands_period),
67 tr_sma: SMA::new(bands_period * 2 - 1),
68 mid_line_wma: WMA::new(mid_line_length),
69 bands_deviation,
70 low_band_adjust,
71 prev_close: None,
72 }
73 }
74}
75
76impl Next<(f64, f64, f64)> for SVEVolatilityBands {
77 type Output = (f64, f64, f64); fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
80 let tr = match self.prev_close {
82 Some(pc) => {
83 let h = high.max(pc);
84 let l = low.min(pc);
85 h - l
86 }
87 None => high - low,
88 };
89 self.prev_close = Some(close);
90
91 let ma_tr = self.tr_sma.next(tr);
92 let wtd_avg_val = self.price_wma.next(close);
93
94 let typical_price = (high + low + close) / 3.0;
95 let mid_line = self.mid_line_wma.next(typical_price);
96
97 let atr_val = ma_tr * self.bands_deviation;
98
99 let upper = wtd_avg_val + wtd_avg_val * (atr_val / close);
104 let lower = wtd_avg_val - wtd_avg_val * (atr_val * self.low_band_adjust / close);
105
106 (upper, mid_line, lower)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use proptest::prelude::*;
114
115 #[test]
116 fn test_sve_volatility_bands_basic() {
117 let mut sve = SVEVolatilityBands::new(20, 2.4, 0.9, 20);
118 let (upper, mid, lower) = sve.next((105.0, 95.0, 100.0));
119
120 assert_eq!(mid, 100.0);
128 assert_eq!(upper, 124.0);
129 assert_eq!(lower, 78.4);
130 }
131
132 fn sve_volatility_bands_batch(
133 data: &[(f64, f64, f64)],
134 period: usize,
135 dev: f64,
136 adj: f64,
137 mid_len: usize,
138 ) -> Vec<(f64, f64, f64)> {
139 let mut sve = SVEVolatilityBands::new(period, dev, adj, mid_len);
140 data.iter().map(|&x| sve.next(x)).collect()
141 }
142
143 proptest! {
144 #[test]
145 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)) {
146 let mut adj_input = Vec::with_capacity(input.len());
147 for (h, l, c) in input {
148 let h_f: f64 = h;
149 let l_f: f64 = l;
150 let c_f: f64 = c;
151 let high = h_f.max(l_f).max(c_f);
152 let low = l_f.min(h_f).min(c_f);
153 adj_input.push((high, low, c_f));
154 }
155
156 let period = 20;
157 let dev = 2.4;
158 let adj = 0.9;
159 let mid_len = 20;
160
161 let mut sve = SVEVolatilityBands::new(period, dev, adj, mid_len);
162 let streaming_results: Vec<(f64, f64, f64)> = adj_input.iter().map(|&x| sve.next(x)).collect();
163 let batch_results = sve_volatility_bands_batch(&adj_input, period, dev, adj, mid_len);
164
165 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
166 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
167 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
168 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
169 }
170 }
171 }
172}