wickra_core/indicators/
vwap.rs1use std::collections::VecDeque;
8
9use crate::error::{Error, Result};
10use crate::indicators::rolling_moments::RollingSum;
11use crate::ohlcv::Candle;
12use crate::traits::Indicator;
13
14#[derive(Debug, Clone, Default)]
33pub struct Vwap {
34 sum_pv: f64,
35 sum_v: f64,
36 has_emitted: bool,
37}
38
39impl Vwap {
40 pub const fn new() -> Self {
42 Self {
43 sum_pv: 0.0,
44 sum_v: 0.0,
45 has_emitted: false,
46 }
47 }
48
49 pub fn value(&self) -> Option<f64> {
51 if self.sum_v == 0.0 {
52 None
53 } else {
54 Some(self.sum_pv / self.sum_v)
55 }
56 }
57}
58
59impl Indicator for Vwap {
60 type Input = Candle;
61 type Output = f64;
62
63 #[inline]
64 fn update(&mut self, candle: Candle) -> Option<f64> {
65 let tp = candle.typical_price();
66 self.sum_pv += tp * candle.volume;
67 self.sum_v += candle.volume;
68 if self.sum_v == 0.0 {
69 return None;
70 }
71 self.has_emitted = true;
72 Some(self.sum_pv / self.sum_v)
73 }
74
75 fn reset(&mut self) {
76 self.sum_pv = 0.0;
77 self.sum_v = 0.0;
78 self.has_emitted = false;
79 }
80
81 #[inline]
82 fn warmup_period(&self) -> usize {
83 1
84 }
85
86 #[inline]
87 fn is_ready(&self) -> bool {
88 self.has_emitted
89 }
90
91 #[inline]
92 fn name(&self) -> &'static str {
93 "VWAP"
94 }
95}
96
97#[derive(Debug, Clone)]
116pub struct RollingVwap {
117 period: usize,
118 window: VecDeque<(f64, f64)>, sum_pv: RollingSum,
120 sum_v: RollingSum,
121}
122
123impl RollingVwap {
124 pub fn new(period: usize) -> Result<Self> {
127 if period == 0 {
128 return Err(Error::PeriodZero);
129 }
130 if period > crate::error::MAX_PERIOD {
131 return Err(Error::InvalidPeriod {
132 message: crate::error::PERIOD_ABOVE_MAX,
133 });
134 }
135 Ok(Self {
136 period,
137 window: VecDeque::with_capacity(period),
138 sum_pv: RollingSum::new(),
139 sum_v: RollingSum::new(),
140 })
141 }
142
143 pub const fn period(&self) -> usize {
145 self.period
146 }
147}
148
149impl Indicator for RollingVwap {
150 type Input = Candle;
151 type Output = f64;
152
153 #[inline]
154 fn update(&mut self, candle: Candle) -> Option<f64> {
155 let pv = candle.typical_price() * candle.volume;
156 if self.window.len() == self.period {
157 let (old_pv, old_v) = self.window.pop_front().expect("non-empty");
158 self.sum_pv.evict(old_pv);
159 self.sum_v.evict(old_v);
160 }
161 self.window.push_back((pv, candle.volume));
162 self.sum_pv.push(pv);
163 self.sum_v.push(candle.volume);
164 if self.sum_pv.needs_reseed(self.period) {
165 self.sum_pv.reseed(self.window.iter().map(|&(p, _)| p));
166 self.sum_v.reseed(self.window.iter().map(|&(_, v)| v));
167 }
168 if self.window.len() < self.period || self.sum_v.value() == 0.0 {
169 return None;
170 }
171 Some(self.sum_pv.value() / self.sum_v.value())
172 }
173
174 fn reset(&mut self) {
175 self.window.clear();
176 self.sum_pv.reset();
177 self.sum_v.reset();
178 }
179
180 #[inline]
181 fn warmup_period(&self) -> usize {
182 self.period
183 }
184
185 #[inline]
186 fn is_ready(&self) -> bool {
187 self.window.len() == self.period && self.sum_v.value() > 0.0
188 }
189
190 #[inline]
191 fn name(&self) -> &'static str {
192 "RollingVWAP"
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::traits::BatchExt;
200 use approx::assert_relative_eq;
201
202 fn c(price: f64, volume: f64) -> Candle {
203 Candle::new(price, price, price, price, volume, 0).unwrap()
204 }
205
206 #[test]
207 fn cumulative_vwap_equal_volumes_equals_mean() {
208 let candles = vec![c(10.0, 1.0), c(20.0, 1.0), c(30.0, 1.0)];
209 let mut v = Vwap::new();
210 let out = v.batch(&candles);
211 assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-12);
212 }
213
214 #[test]
218 fn cumulative_value_some_branch_after_update() {
219 let mut v = Vwap::new();
220 v.update(c(42.0, 5.0));
222 assert_relative_eq!(v.value().expect("non-zero volume"), 42.0, epsilon = 1e-12);
223 }
224
225 #[test]
230 fn cumulative_zero_volume_first_candle_returns_none() {
231 let mut v = Vwap::new();
232 let out = v.update(c(42.0, 0.0));
233 assert_eq!(out, None);
234 assert!(!v.is_ready());
235 let out2 = v.update(c(10.0, 4.0));
237 assert_relative_eq!(out2.expect("now warmed"), 10.0, epsilon = 1e-12);
238 }
239
240 #[test]
244 fn cumulative_metadata() {
245 let v = Vwap::new();
246 assert_eq!(v.warmup_period(), 1);
247 assert_eq!(v.name(), "VWAP");
248 }
249
250 #[test]
251 fn cumulative_vwap_weighted() {
252 let candles = vec![c(10.0, 1.0), c(20.0, 3.0)];
254 let mut v = Vwap::new();
255 let out = v.batch(&candles);
256 assert_relative_eq!(out[1].unwrap(), 17.5, epsilon = 1e-12);
257 }
258
259 #[test]
264 fn rolling_accessors_and_metadata() {
265 let v = RollingVwap::new(7).unwrap();
266 assert_eq!(v.period(), 7);
267 assert_eq!(v.warmup_period(), 7);
268 assert_eq!(v.name(), "RollingVWAP");
269 }
270
271 #[test]
272 fn rolling_vwap_window_slides() {
273 let candles = vec![c(10.0, 1.0), c(20.0, 1.0), c(30.0, 1.0), c(40.0, 1.0)];
274 let mut v = RollingVwap::new(3).unwrap();
275 let out = v.batch(&candles);
276 assert!(out[1].is_none());
277 assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-12);
279 assert_relative_eq!(out[3].unwrap(), 30.0, epsilon = 1e-12);
281 }
282
283 #[test]
284 fn batch_equals_streaming_cumulative() {
285 let candles: Vec<Candle> = (1..20).map(|i| c(f64::from(i), 1.0)).collect();
286 let mut a = Vwap::new();
287 let mut b = Vwap::new();
288 assert_eq!(
289 a.batch(&candles),
290 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
291 );
292 }
293
294 #[test]
295 fn batch_equals_streaming_rolling() {
296 let candles: Vec<Candle> = (1..30)
297 .map(|i| c(f64::from(i), f64::from(i % 5 + 1)))
298 .collect();
299 let mut a = RollingVwap::new(10).unwrap();
300 let mut b = RollingVwap::new(10).unwrap();
301 assert_eq!(
302 a.batch(&candles),
303 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
304 );
305 }
306
307 #[test]
308 fn rolling_rejects_zero_period() {
309 assert!(RollingVwap::new(0).is_err());
310 }
311
312 #[test]
313 fn cumulative_reset_clears_state() {
314 let candles = vec![c(10.0, 1.0), c(20.0, 1.0), c(30.0, 1.0)];
315 let mut v = Vwap::new();
316 v.batch(&candles);
317 assert!(v.is_ready());
318 v.reset();
319 assert!(!v.is_ready());
320 assert_eq!(v.value(), None);
321 }
322
323 #[test]
324 fn rolling_reset_clears_state() {
325 let candles: Vec<Candle> = (1..=10).map(|i| c(f64::from(i), 1.0)).collect();
326 let mut v = RollingVwap::new(5).unwrap();
327 v.batch(&candles);
328 assert!(v.is_ready());
329 v.reset();
330 assert!(!v.is_ready());
331 assert_eq!(v.update(candles[0]), None);
332 }
333}