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