nautilus_indicators/average/
vwap.rs1use std::fmt::{Display, Formatter};
17
18use nautilus_model::data::Bar;
19
20use crate::indicator::Indicator;
21
22#[repr(C)]
23#[derive(Debug, Default)]
24#[cfg_attr(
25 feature = "python",
26 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
27)]
28pub struct VolumeWeightedAveragePrice {
29 pub value: f64,
30 pub initialized: bool,
31 has_inputs: bool,
32 price_volume: f64,
33 volume_total: f64,
34 day: i64,
35}
36
37impl Indicator for VolumeWeightedAveragePrice {
38 fn name(&self) -> String {
39 stringify!(VolumeWeightedAveragePrice).to_string()
40 }
41
42 fn has_inputs(&self) -> bool {
43 self.has_inputs
44 }
45
46 fn initialized(&self) -> bool {
47 self.initialized
48 }
49
50 fn handle_bar(&mut self, bar: &Bar) {
51 let typical_price = (bar.close.as_f64() + bar.high.as_f64() + bar.low.as_f64()) / 3.0;
52
53 self.update_raw(typical_price, (&bar.volume).into(), bar.ts_init.as_f64());
54 }
55
56 fn reset(&mut self) {
57 self.value = 0.0;
58 self.has_inputs = false;
59 self.initialized = false;
60 self.day = -1;
61 self.price_volume = 0.0;
62 self.volume_total = 0.0;
63 }
64}
65
66impl VolumeWeightedAveragePrice {
67 #[must_use]
69 pub const fn new() -> Self {
70 Self {
71 value: 0.0,
72 initialized: false,
73 has_inputs: false,
74 price_volume: 0.0,
75 volume_total: 0.0,
76 day: -1,
77 }
78 }
79
80 pub fn update_raw(&mut self, price: f64, volume: f64, timestamp: f64) {
81 const SECONDS_PER_DAY: f64 = 86_400.0;
82 let epoch_day = (timestamp / SECONDS_PER_DAY).floor() as i64;
83
84 if epoch_day != self.day {
85 self.reset();
86 self.day = epoch_day;
87 self.value = price;
88 }
89
90 if !self.initialized {
91 self.has_inputs = true;
92 self.initialized = true;
93 }
94
95 if volume == 0.0 {
96 return;
97 }
98
99 self.price_volume += price * volume;
100 self.volume_total += volume;
101 self.value = self.price_volume / self.volume_total;
102 }
103}
104
105impl Display for VolumeWeightedAveragePrice {
106 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107 write!(f, "{}", self.name())
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use nautilus_model::data::Bar;
114 use rstest::rstest;
115
116 use crate::{average::vwap::VolumeWeightedAveragePrice, indicator::Indicator, stubs::*};
117
118 const SECONDS_PER_DAY: f64 = 86_400.0;
119 const DAY0: f64 = 10.0;
120 const DAY1: f64 = SECONDS_PER_DAY;
121
122 #[rstest]
123 fn test_vwap_initialized(indicator_vwap: VolumeWeightedAveragePrice) {
124 let display_st = format!("{indicator_vwap}");
125 assert_eq!(display_st, "VolumeWeightedAveragePrice");
126 assert!(!indicator_vwap.initialized());
127 assert!(!indicator_vwap.has_inputs());
128 }
129
130 #[rstest]
131 fn test_value_with_one_input(mut indicator_vwap: VolumeWeightedAveragePrice) {
132 indicator_vwap.update_raw(10.0, 10.0, DAY0);
133 assert_eq!(indicator_vwap.value, 10.0);
134 }
135
136 #[rstest]
137 fn test_value_with_three_inputs_on_the_same_day(
138 mut indicator_vwap: VolumeWeightedAveragePrice,
139 ) {
140 indicator_vwap.update_raw(10.0, 10.0, DAY0);
141 indicator_vwap.update_raw(20.0, 20.0, DAY0 + 1.0);
142 indicator_vwap.update_raw(30.0, 30.0, DAY0 + 2.0);
143 assert!((indicator_vwap.value - 23.333_333_333_333_332).abs() < 1e-12);
144 }
145
146 #[rstest]
147 fn test_value_with_three_inputs_on_different_days(
148 mut indicator_vwap: VolumeWeightedAveragePrice,
149 ) {
150 indicator_vwap.update_raw(10.0, 10.0, DAY0);
151 indicator_vwap.update_raw(20.0, 20.0, DAY1);
152 indicator_vwap.update_raw(30.0, 30.0, DAY0);
153 assert_eq!(indicator_vwap.value, 30.0);
154 }
155
156 #[rstest]
157 fn test_value_with_ten_inputs(mut indicator_vwap: VolumeWeightedAveragePrice) {
158 for i in 0..10 {
159 let price = 0.00010f64.mul_add(f64::from(i), 1.00000);
160 let volume = 1.0 + f64::from(i % 3);
161 indicator_vwap.update_raw(price, volume, DAY0);
162 }
163 indicator_vwap.update_raw(1.00000, 2.00000, DAY0);
164 assert!((indicator_vwap.value - 1.000_414_285_714_286).abs() < 1e-12);
165 }
166
167 #[rstest]
168 fn test_handle_bar(
169 mut indicator_vwap: VolumeWeightedAveragePrice,
170 bar_ethusdt_binance_minute_bid: Bar,
171 ) {
172 indicator_vwap.handle_bar(&bar_ethusdt_binance_minute_bid);
173 assert_eq!(indicator_vwap.value, 1522.333333333333);
174 assert!(indicator_vwap.initialized);
175 }
176
177 #[rstest]
178 fn test_reset(mut indicator_vwap: VolumeWeightedAveragePrice) {
179 indicator_vwap.update_raw(10.0, 10.0, DAY0);
180 indicator_vwap.reset();
181 assert_eq!(indicator_vwap.value, 0.0);
182 assert!(!indicator_vwap.has_inputs);
183 assert!(!indicator_vwap.initialized);
184 }
185
186 #[rstest]
187 fn test_reset_on_exact_day_boundary() {
188 let mut vwap = VolumeWeightedAveragePrice::new();
189
190 vwap.update_raw(100.0, 5.0, DAY0);
191 let old = vwap.value;
192
193 vwap.update_raw(200.0, 5.0, DAY1);
194 assert_eq!(vwap.value, 200.0);
195 assert_ne!(vwap.value, old);
196 }
197
198 #[rstest]
199 fn test_no_reset_within_same_day() {
200 let mut vwap = VolumeWeightedAveragePrice::new();
201 vwap.update_raw(100.0, 5.0, DAY0);
202
203 vwap.update_raw(200.0, 5.0, DAY0 + 1.0);
204 assert!(vwap.value > 100.0 && vwap.value < 200.0);
205 }
206
207 #[rstest]
208 fn test_zero_volume_does_not_change_value() {
209 let mut vwap = VolumeWeightedAveragePrice::new();
210 vwap.update_raw(100.0, 10.0, DAY0);
211 let before = vwap.value;
212
213 vwap.update_raw(9999.0, 0.0, DAY0);
214 assert_eq!(vwap.value, before);
215 }
216
217 #[rstest]
218 fn test_epoch_day_floor_rounding() {
219 let mut vwap = VolumeWeightedAveragePrice::new();
220
221 vwap.update_raw(50.0, 5.0, DAY1 - 0.000_001);
222 let before = vwap.value;
223
224 vwap.update_raw(150.0, 5.0, DAY1);
225 assert_eq!(vwap.value, 150.0);
226 assert_ne!(vwap.value, before);
227 }
228
229 #[rstest]
230 fn test_reset_when_timestamp_goes_backwards() {
231 let mut vwap = VolumeWeightedAveragePrice::new();
232 vwap.update_raw(10.0, 10.0, DAY0);
233 vwap.update_raw(20.0, 10.0, DAY1);
234 vwap.update_raw(30.0, 10.0, DAY0);
235 assert_eq!(vwap.value, 30.0);
236 }
237
238 #[rstest]
239 #[case(10.0, 11.0)]
240 #[case(43_200.123, 86_399.999)]
241 fn test_no_reset_for_same_epoch_day(#[case] t1: f64, #[case] t2: f64) {
242 let mut vwap = VolumeWeightedAveragePrice::new();
243
244 vwap.update_raw(100.0, 10.0, t1);
245 let before = vwap.value;
246
247 vwap.update_raw(200.0, 10.0, t2);
248
249 assert!(vwap.value > before && vwap.value < 200.0);
250 }
251
252 #[rstest]
253 #[case(86_399.999, 86_400.0)]
254 #[case(86_400.0, 172_800.0)]
255 fn test_reset_when_epoch_day_changes(#[case] t1: f64, #[case] t2: f64) {
256 let mut vwap = VolumeWeightedAveragePrice::new();
257
258 vwap.update_raw(100.0, 10.0, t1);
259
260 vwap.update_raw(200.0, 10.0, t2);
261
262 assert_eq!(vwap.value, 200.0);
263 }
264
265 #[rstest]
266 fn test_first_input_zero_volume_does_not_divide_by_zero() {
267 let mut vwap = VolumeWeightedAveragePrice::new();
268
269 vwap.update_raw(100.0, 0.0, DAY0);
270 assert_eq!(vwap.value, 100.0);
271 assert!(vwap.initialized());
272
273 vwap.update_raw(200.0, 10.0, DAY0 + 1.0);
274 assert_eq!(vwap.value, 200.0);
275 }
276
277 #[rstest]
278 fn test_zero_volume_day_rollover_resets_and_seeds() {
279 let mut vwap = VolumeWeightedAveragePrice::new();
280 vwap.update_raw(100.0, 10.0, DAY0);
281
282 vwap.update_raw(9999.0, 0.0, DAY1);
283 assert_eq!(vwap.value, 9999.0);
284 }
285
286 #[rstest]
287 fn test_handle_bar_matches_update_raw(
288 mut indicator_vwap: VolumeWeightedAveragePrice,
289 bar_ethusdt_binance_minute_bid: Bar,
290 ) {
291 indicator_vwap.handle_bar(&bar_ethusdt_binance_minute_bid);
292
293 let tp = (bar_ethusdt_binance_minute_bid.close.as_f64()
294 + bar_ethusdt_binance_minute_bid.high.as_f64()
295 + bar_ethusdt_binance_minute_bid.low.as_f64())
296 / 3.0;
297
298 let mut vwap_raw = VolumeWeightedAveragePrice::new();
299 vwap_raw.update_raw(
300 tp,
301 (&bar_ethusdt_binance_minute_bid.volume).into(),
302 bar_ethusdt_binance_minute_bid.ts_init.as_f64(),
303 );
304
305 assert!((indicator_vwap.value - vwap_raw.value).abs() < 1e-12);
306 }
307
308 #[rstest]
309 #[case(1.0e-9, 1.0e-9)]
310 #[case(1.0e9, 1.0e6)]
311 #[case(42.4242, std::f64::consts::PI)]
312 fn test_extreme_prices_and_volumes_do_not_overflow(#[case] price: f64, #[case] volume: f64) {
313 let mut vwap = VolumeWeightedAveragePrice::new();
314 vwap.update_raw(price, volume, DAY0);
315 assert_eq!(vwap.value, price);
316 }
317
318 #[rstest]
319 fn negative_timestamp() {
320 let mut vwap = VolumeWeightedAveragePrice::new();
321 vwap.update_raw(42.0, 1.0, -1.0);
322 assert_eq!(vwap.value, 42.0);
323 vwap.update_raw(43.0, 1.0, -1.0);
324 assert!(vwap.value > 42.0 && vwap.value < 43.0);
325 }
326
327 #[rstest]
328 fn huge_future_timestamp_saturates() {
329 let ts = 1.0e20;
330 let mut vwap = VolumeWeightedAveragePrice::new();
331 vwap.update_raw(1.0, 1.0, ts);
332 vwap.update_raw(2.0, 1.0, ts + 1.0);
333 assert!(vwap.value > 1.0 && vwap.value < 2.0);
334 }
335
336 #[rstest]
337 fn negative_volume_changes_sign() {
338 let mut vwap = VolumeWeightedAveragePrice::new();
339 vwap.update_raw(100.0, 10.0, 0.0);
340 vwap.update_raw(200.0, -10.0, 0.0);
341 assert_eq!(vwap.volume_total, 0.0);
342 }
343
344 #[rstest]
345 fn nan_volume_propagates() {
346 let mut vwap = VolumeWeightedAveragePrice::new();
347 vwap.update_raw(100.0, 1.0, 0.0);
348 vwap.update_raw(200.0, f64::NAN, 0.0);
349 assert!(vwap.value.is_nan());
350 }
351
352 #[rstest]
353 fn zero_and_negative_price() {
354 let mut vwap = VolumeWeightedAveragePrice::new();
355 vwap.update_raw(0.0, 5.0, 0.0);
356 assert_eq!(vwap.value, 0.0);
357 vwap.update_raw(-10.0, 5.0, 0.0);
358 assert!(vwap.value < 0.0);
359 }
360
361 #[rstest]
362 fn nan_price_propagates() {
363 let mut vwap = VolumeWeightedAveragePrice::new();
364 vwap.update_raw(f64::NAN, 1.0, 0.0);
365 assert!(vwap.value.is_nan());
366 }
367}