wickra_core/indicators/
vwma.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
42pub struct Vwma {
43 period: usize,
44 window: VecDeque<(f64, f64)>,
46 sum_pv: RollingSum,
47 sum_v: RollingSum,
48 sum_close: RollingSum,
49 current: Option<f64>,
50}
51
52impl Vwma {
53 pub fn new(period: usize) -> Result<Self> {
59 if period == 0 {
60 return Err(Error::PeriodZero);
61 }
62 if period > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 Ok(Self {
68 period,
69 window: VecDeque::with_capacity(period),
70 sum_pv: RollingSum::new(),
71 sum_v: RollingSum::new(),
72 sum_close: RollingSum::new(),
73 current: None,
74 })
75 }
76
77 pub const fn period(&self) -> usize {
79 self.period
80 }
81
82 pub const fn value(&self) -> Option<f64> {
84 self.current
85 }
86}
87
88impl Indicator for Vwma {
89 type Input = Candle;
90 type Output = f64;
91
92 #[inline]
93 fn update(&mut self, candle: Candle) -> Option<f64> {
94 let close = candle.close;
95 let volume = candle.volume;
96 if self.window.len() == self.period {
97 let (old_close, old_volume) = self.window.pop_front().expect("window is non-empty");
98 self.sum_pv.evict(old_close * old_volume);
99 self.sum_v.evict(old_volume);
100 self.sum_close.evict(old_close);
101 }
102 self.window.push_back((close, volume));
103 self.sum_pv.push(close * volume);
104 self.sum_v.push(volume);
105 self.sum_close.push(close);
106 if self.sum_pv.needs_reseed(self.period) {
107 self.sum_pv.reseed(self.window.iter().map(|&(c, v)| c * v));
108 self.sum_v.reseed(self.window.iter().map(|&(_, v)| v));
109 self.sum_close.reseed(self.window.iter().map(|&(c, _)| c));
110 }
111 if self.window.len() < self.period {
112 return None;
113 }
114 let value = if self.sum_v.value() > 0.0 {
115 self.sum_pv.value() / self.sum_v.value()
116 } else {
117 self.sum_close.value() / self.period as f64
120 };
121 self.current = Some(value);
122 Some(value)
123 }
124
125 fn reset(&mut self) {
126 self.window.clear();
127 self.sum_pv.reset();
128 self.sum_v.reset();
129 self.sum_close.reset();
130 self.current = None;
131 }
132
133 #[inline]
134 fn warmup_period(&self) -> usize {
135 self.period
136 }
137
138 #[inline]
139 fn is_ready(&self) -> bool {
140 self.current.is_some()
141 }
142
143 #[inline]
144 fn name(&self) -> &'static str {
145 "VWMA"
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::traits::BatchExt;
153 use approx::assert_relative_eq;
154
155 fn candle(close: f64, volume: f64, ts: i64) -> Candle {
157 Candle::new(close, close, close, close, volume, ts).unwrap()
158 }
159
160 #[test]
161 fn new_rejects_zero_period() {
162 assert!(matches!(Vwma::new(0), Err(Error::PeriodZero)));
163 }
164
165 #[test]
169 fn accessors_and_metadata() {
170 let mut v = Vwma::new(5).unwrap();
171 assert_eq!(v.period(), 5);
172 assert_eq!(v.name(), "VWMA");
173 assert_eq!(v.value(), None);
174 for i in 1..=5i64 {
175 let p = 100.0 + i as f64;
176 v.update(Candle::new(p, p, p, p, 1.0, i).unwrap());
177 }
178 assert!(v.value().is_some());
179 }
180
181 #[test]
182 fn reference_value() {
183 let mut vwma = Vwma::new(2).unwrap();
185 assert_eq!(vwma.update(candle(10.0, 1.0, 0)), None);
186 assert_relative_eq!(
187 vwma.update(candle(20.0, 3.0, 1)).unwrap(),
188 17.5,
189 epsilon = 1e-12
190 );
191 assert_relative_eq!(
193 vwma.update(candle(30.0, 1.0, 2)).unwrap(),
194 22.5,
195 epsilon = 1e-12
196 );
197 }
198
199 #[test]
200 fn zero_volume_window_falls_back_to_unweighted_mean() {
201 let mut vwma = Vwma::new(2).unwrap();
202 assert_eq!(vwma.update(candle(10.0, 0.0, 0)), None);
203 assert_relative_eq!(
205 vwma.update(candle(20.0, 0.0, 1)).unwrap(),
206 15.0,
207 epsilon = 1e-12
208 );
209 }
210
211 #[test]
212 fn constant_series_yields_the_constant() {
213 let mut vwma = Vwma::new(5).unwrap();
214 let candles: Vec<Candle> = (0..30).map(|i| candle(42.0, 3.0, i)).collect();
215 let out = vwma.batch(&candles);
216 for x in out.iter().skip(4).flatten() {
217 assert_relative_eq!(*x, 42.0, epsilon = 1e-12);
218 }
219 }
220
221 #[test]
222 fn high_volume_bar_pulls_the_average() {
223 let mut vwma = Vwma::new(3).unwrap();
225 vwma.update(candle(10.0, 1.0, 0));
226 vwma.update(candle(10.0, 1.0, 1));
227 let v = vwma.update(candle(20.0, 100.0, 2)).unwrap();
228 let simple_mean = (10.0 + 10.0 + 20.0) / 3.0;
229 assert!(
230 v > simple_mean,
231 "{v} should exceed simple mean {simple_mean}"
232 );
233 }
234
235 #[test]
236 fn first_emission_at_warmup_period() {
237 let mut vwma = Vwma::new(4).unwrap();
238 assert_eq!(vwma.warmup_period(), 4);
239 for i in 0..3 {
240 assert_eq!(vwma.update(candle(10.0, 1.0, i)), None);
241 }
242 assert!(vwma.update(candle(10.0, 1.0, 3)).is_some());
243 }
244
245 #[test]
246 fn reset_clears_state() {
247 let mut vwma = Vwma::new(3).unwrap();
248 let candles: Vec<Candle> = (0..10).map(|i| candle(10.0 + i as f64, 2.0, i)).collect();
249 vwma.batch(&candles);
250 assert!(vwma.is_ready());
251 vwma.reset();
252 assert!(!vwma.is_ready());
253 assert_eq!(vwma.update(candle(10.0, 1.0, 0)), None);
254 }
255
256 #[test]
257 fn batch_equals_streaming() {
258 let candles: Vec<Candle> = (0..50_i64)
259 .map(|i| {
260 let c = 100.0 + (i as f64 * 0.3).sin() * 8.0;
261 candle(c, 1.0 + (i % 7) as f64, i)
262 })
263 .collect();
264 let batch = Vwma::new(8).unwrap().batch(&candles);
265 let mut b = Vwma::new(8).unwrap();
266 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
267 assert_eq!(batch, streamed);
268 }
269}