mantis_ta/indicators/volume/
vwap.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
45pub struct VWAP {
46 period: usize,
47 window: RingBuf<(f64, f64)>,
48 sum_tp_vol: f64,
49 sum_vol: f64,
50}
51
52impl VWAP {
53 pub fn new(period: usize) -> Self {
54 assert!(period > 0, "period must be > 0");
55 Self {
56 period,
57 window: RingBuf::new(period, (0.0, 0.0)),
58 sum_tp_vol: 0.0,
59 sum_vol: 0.0,
60 }
61 }
62
63 #[inline]
64 fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> Option<f64> {
65 let typical_price = (high + low + close) / 3.0;
66 let tp_vol = typical_price * volume;
67
68 if let Some((old_tp_vol, old_vol)) = self.window.push((tp_vol, volume)) {
69 self.sum_tp_vol -= old_tp_vol;
70 self.sum_vol -= old_vol;
71 }
72 self.sum_tp_vol += tp_vol;
73 self.sum_vol += volume;
74
75 if self.window.len() < self.period {
76 None
77 } else if self.sum_vol == 0.0 {
78 Some(typical_price)
79 } else {
80 Some(self.sum_tp_vol / self.sum_vol)
81 }
82 }
83}
84
85impl Indicator for VWAP {
86 type Output = f64;
87
88 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
89 self.update(candle.high, candle.low, candle.close, candle.volume)
90 }
91
92 fn reset(&mut self) {
93 self.window = RingBuf::new(self.period, (0.0, 0.0));
94 self.sum_tp_vol = 0.0;
95 self.sum_vol = 0.0;
96 }
97
98 fn warmup_period(&self) -> usize {
99 self.period
100 }
101
102 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
103 Box::new(self.clone())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn candle(h: f64, l: f64, c: f64, v: f64) -> Candle {
112 Candle {
113 timestamp: 0,
114 open: c,
115 high: h,
116 low: l,
117 close: c,
118 volume: v,
119 }
120 }
121
122 #[test]
123 fn vwap_warmup_period_is_period() {
124 let vwap = VWAP::new(14);
125 assert_eq!(vwap.warmup_period(), 14);
126 }
127
128 #[test]
129 fn vwap_emits_none_until_warmup() {
130 let mut vwap = VWAP::new(3);
131 let candles = vec![
132 candle(11.0, 9.0, 10.0, 100.0),
133 candle(12.0, 10.0, 11.0, 110.0),
134 ];
135 let outputs: Vec<_> = candles.iter().map(|c| vwap.next(c)).collect();
136 assert!(outputs.iter().all(|v| v.is_none()));
137 }
138
139 #[test]
140 fn vwap_matches_manual_calculation() {
141 let mut vwap = VWAP::new(3);
142 let candles = vec![
143 candle(11.0, 9.0, 10.0, 100.0),
144 candle(12.0, 10.0, 11.0, 200.0),
145 candle(13.0, 11.0, 12.0, 300.0),
146 ];
147 let outputs: Vec<_> = candles.iter().map(|c| vwap.next(c)).collect();
148
149 let expected = (10.0 * 100.0 + 11.0 * 200.0 + 12.0 * 300.0) / (100.0 + 200.0 + 300.0);
150 assert!((outputs[2].unwrap() - expected).abs() < 1e-9);
151 }
152
153 #[test]
154 fn vwap_flat_prices_equals_typical_price() {
155 let mut vwap = VWAP::new(3);
156 let candles = vec![candle(51.0, 49.0, 50.0, 100.0); 3];
157 let outputs: Vec<_> = candles.iter().map(|c| vwap.next(c)).collect();
158 assert!((outputs[2].unwrap() - 50.0).abs() < 1e-9);
159 }
160
161 #[test]
162 fn vwap_zero_volume_window_falls_back_to_typical_price() {
163 let mut vwap = VWAP::new(2);
164 let candles = vec![candle(11.0, 9.0, 10.0, 0.0), candle(21.0, 19.0, 20.0, 0.0)];
165 let outputs: Vec<_> = candles.iter().map(|c| vwap.next(c)).collect();
166 assert!((outputs[1].unwrap() - 20.0).abs() < 1e-9);
167 }
168
169 #[test]
170 fn vwap_bounded_by_window_extremes() {
171 let mut vwap = VWAP::new(4);
174 let candles = vec![
175 candle(11.0, 9.0, 10.0, 50.0),
176 candle(21.0, 19.0, 20.0, 500.0),
177 candle(16.0, 14.0, 15.0, 10.0),
178 candle(31.0, 29.0, 30.0, 200.0),
179 ];
180 let outputs: Vec<_> = candles.iter().map(|c| vwap.next(c)).collect();
181 let v = outputs[3].unwrap();
182 assert!((10.0..=30.0).contains(&v));
183 }
184
185 #[test]
186 fn vwap_streaming_matches_batch() {
187 let candles: Vec<Candle> = (0..15)
188 .map(|i| {
189 candle(
190 101.0 + i as f64,
191 99.0 + i as f64,
192 100.0 + i as f64,
193 1_000.0 + i as f64 * 10.0,
194 )
195 })
196 .collect();
197
198 let batch = VWAP::new(5).calculate(&candles);
199
200 let mut streamed_vwap = VWAP::new(5);
201 let streamed: Vec<_> = candles.iter().map(|c| streamed_vwap.next(c)).collect();
202
203 assert_eq!(streamed, batch);
204 }
205
206 #[test]
207 fn vwap_reset_clears_state() {
208 let mut vwap = VWAP::new(4);
209 let candles: Vec<Candle> = (0..6)
210 .map(|i| candle(101.0 + i as f64, 99.0 + i as f64, 100.0 + i as f64, 1_000.0))
211 .collect();
212 for c in &candles {
213 vwap.next(c);
214 }
215 vwap.reset();
216
217 let mut fresh = VWAP::new(4);
218 for c in &candles {
219 assert_eq!(vwap.next(c), fresh.next(c));
220 }
221 }
222
223 #[test]
224 #[should_panic(expected = "period must be > 0")]
225 fn vwap_rejects_zero_period() {
226 VWAP::new(0);
227 }
228}