wickra_core/indicators/
vwap_stddev_bands.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct VwapStdDevBandsOutput {
10 pub upper: f64,
12 pub middle: f64,
14 pub lower: f64,
16 pub stddev: f64,
18}
19
20#[derive(Debug, Clone)]
54pub struct VwapStdDevBands {
55 multiplier: f64,
56 sum_pv: f64,
57 sum_p2v: f64,
58 sum_v: f64,
59 has_emitted: bool,
60}
61
62impl VwapStdDevBands {
63 pub fn new(multiplier: f64) -> Result<Self> {
67 if !multiplier.is_finite() || multiplier <= 0.0 {
68 return Err(Error::NonPositiveMultiplier);
69 }
70 Ok(Self {
71 multiplier,
72 sum_pv: 0.0,
73 sum_p2v: 0.0,
74 sum_v: 0.0,
75 has_emitted: false,
76 })
77 }
78
79 pub const fn multiplier(&self) -> f64 {
81 self.multiplier
82 }
83}
84
85impl Indicator for VwapStdDevBands {
86 type Input = Candle;
87 type Output = VwapStdDevBandsOutput;
88
89 fn update(&mut self, candle: Candle) -> Option<VwapStdDevBandsOutput> {
90 let tp = candle.typical_price();
91 self.sum_pv += tp * candle.volume;
92 self.sum_p2v += tp * tp * candle.volume;
93 self.sum_v += candle.volume;
94 if self.sum_v == 0.0 {
95 return None;
96 }
97 self.has_emitted = true;
98 let vwap = self.sum_pv / self.sum_v;
99 let var = (self.sum_p2v / self.sum_v - vwap * vwap).max(0.0);
102 let sigma = var.sqrt();
103 Some(VwapStdDevBandsOutput {
104 upper: vwap + self.multiplier * sigma,
105 middle: vwap,
106 lower: vwap - self.multiplier * sigma,
107 stddev: sigma,
108 })
109 }
110
111 fn reset(&mut self) {
112 self.sum_pv = 0.0;
113 self.sum_p2v = 0.0;
114 self.sum_v = 0.0;
115 self.has_emitted = false;
116 }
117
118 fn warmup_period(&self) -> usize {
119 1
120 }
121
122 fn is_ready(&self) -> bool {
123 self.has_emitted
124 }
125
126 fn name(&self) -> &'static str {
127 "VwapStdDevBands"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135 use approx::assert_relative_eq;
136
137 fn c(h: f64, l: f64, cl: f64, v: f64) -> Candle {
138 Candle::new(cl, h, l, cl, v, 0).unwrap()
139 }
140
141 #[test]
142 fn rejects_non_positive_multiplier() {
143 assert!(matches!(
144 VwapStdDevBands::new(0.0),
145 Err(Error::NonPositiveMultiplier)
146 ));
147 assert!(matches!(
148 VwapStdDevBands::new(-1.0),
149 Err(Error::NonPositiveMultiplier)
150 ));
151 assert!(matches!(
152 VwapStdDevBands::new(f64::NAN),
153 Err(Error::NonPositiveMultiplier)
154 ));
155 }
156
157 #[test]
158 fn accessors_and_metadata() {
159 let v = VwapStdDevBands::new(2.0).unwrap();
160 assert_relative_eq!(v.multiplier(), 2.0, epsilon = 1e-12);
161 assert_eq!(v.warmup_period(), 1);
162 assert_eq!(v.name(), "VwapStdDevBands");
163 }
164
165 #[test]
166 fn zero_volume_returns_none() {
167 let mut v = VwapStdDevBands::new(2.0).unwrap();
168 assert!(v.update(c(10.0, 10.0, 10.0, 0.0)).is_none());
169 }
170
171 #[test]
172 fn constant_price_collapses_bands() {
173 let candles: Vec<Candle> = (0..10).map(|_| c(10.0, 10.0, 10.0, 5.0)).collect();
174 let mut v = VwapStdDevBands::new(2.0).unwrap();
175 let last = v.batch(&candles).into_iter().flatten().last().unwrap();
176 assert_relative_eq!(last.middle, 10.0, epsilon = 1e-9);
177 assert_relative_eq!(last.stddev, 0.0, epsilon = 1e-9);
178 assert_relative_eq!(last.upper, 10.0, epsilon = 1e-9);
179 assert_relative_eq!(last.lower, 10.0, epsilon = 1e-9);
180 }
181
182 #[test]
183 fn upper_above_middle_above_lower() {
184 let candles: Vec<Candle> = (0..50)
185 .map(|i| {
186 let m = 100.0 + (f64::from(i) * 0.2).sin() * 5.0;
187 c(m + 1.0, m - 1.0, m, 1.0 + f64::from(i % 5))
188 })
189 .collect();
190 let mut v = VwapStdDevBands::new(2.0).unwrap();
191 for o in v.batch(&candles).into_iter().flatten() {
192 assert!(o.upper >= o.middle);
193 assert!(o.middle >= o.lower);
194 assert!(o.stddev >= 0.0);
195 }
196 }
197
198 #[test]
199 fn batch_equals_streaming() {
200 let candles: Vec<Candle> = (0..40)
201 .map(|i| {
202 c(
203 f64::from(i) + 2.0,
204 f64::from(i),
205 f64::from(i) + 1.0,
206 1.0 + f64::from(i % 4),
207 )
208 })
209 .collect();
210 let mut a = VwapStdDevBands::new(2.0).unwrap();
211 let mut b = VwapStdDevBands::new(2.0).unwrap();
212 assert_eq!(
213 a.batch(&candles),
214 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
215 );
216 }
217
218 #[test]
219 fn reset_clears_state() {
220 let candles: Vec<Candle> = (0..10)
221 .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i), 1.0))
222 .collect();
223 let mut v = VwapStdDevBands::new(2.0).unwrap();
224 v.batch(&candles);
225 assert!(v.is_ready());
226 v.reset();
227 assert!(!v.is_ready());
228 assert_eq!(v.update(c(10.0, 10.0, 10.0, 0.0)), None);
231 }
232
233 #[test]
238 fn reference_values() {
239 let candles = [c(8.0, 8.0, 8.0, 1.0), c(12.0, 12.0, 12.0, 1.0)];
243 let mut v = VwapStdDevBands::new(1.5).unwrap();
244 let _ = v.update(candles[0]);
245 let out = v.update(candles[1]).unwrap();
246 assert_relative_eq!(out.middle, 10.0, epsilon = 1e-9);
247 assert_relative_eq!(out.stddev, 2.0, epsilon = 1e-9);
248 assert_relative_eq!(out.upper, 13.0, epsilon = 1e-9);
249 assert_relative_eq!(out.lower, 7.0, epsilon = 1e-9);
250 }
251}