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