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 = "posei_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)]
115mod tests {
116 use nautilus_model::data::Bar;
117 use rstest::rstest;
118
119 use crate::{average::vwap::VolumeWeightedAveragePrice, indicator::Indicator, stubs::*};
120
121 const SECONDS_PER_DAY: f64 = 86_400.0;
122 const DAY0: f64 = 10.0;
123 const DAY1: f64 = SECONDS_PER_DAY;
124
125 #[rstest]
126 fn test_vwap_initialized(indicator_vwap: VolumeWeightedAveragePrice) {
127 let display_st = format!("{indicator_vwap}");
128 assert_eq!(display_st, "VolumeWeightedAveragePrice");
129 assert!(!indicator_vwap.initialized());
130 assert!(!indicator_vwap.has_inputs());
131 }
132
133 #[rstest]
134 fn test_value_with_one_input(mut indicator_vwap: VolumeWeightedAveragePrice) {
135 indicator_vwap.update_raw(10.0, 10.0, DAY0);
136 assert_eq!(indicator_vwap.value, 10.0);
137 }
138
139 #[rstest]
140 fn test_value_with_three_inputs_on_the_same_day(
141 mut indicator_vwap: VolumeWeightedAveragePrice,
142 ) {
143 indicator_vwap.update_raw(10.0, 10.0, DAY0);
144 indicator_vwap.update_raw(20.0, 20.0, DAY0 + 1.0);
145 indicator_vwap.update_raw(30.0, 30.0, DAY0 + 2.0);
146 assert!((indicator_vwap.value - 23.333_333_333_333_332).abs() < 1e-12);
147 }
148
149 #[rstest]
150 fn test_value_with_three_inputs_on_different_days(
151 mut indicator_vwap: VolumeWeightedAveragePrice,
152 ) {
153 indicator_vwap.update_raw(10.0, 10.0, DAY0);
154 indicator_vwap.update_raw(20.0, 20.0, DAY1);
155 indicator_vwap.update_raw(30.0, 30.0, DAY0);
156 assert_eq!(indicator_vwap.value, 30.0);
157 }
158
159 #[rstest]
160 fn test_value_with_ten_inputs(mut indicator_vwap: VolumeWeightedAveragePrice) {
161 for i in 0..10 {
162 let price = 0.00010f64.mul_add(f64::from(i), 1.00000);
163 let volume = 1.0 + f64::from(i % 3);
164 indicator_vwap.update_raw(price, volume, DAY0);
165 }
166 indicator_vwap.update_raw(1.00000, 2.00000, DAY0);
167 assert!((indicator_vwap.value - 1.000_414_285_714_286).abs() < 1e-12);
168 }
169
170 #[rstest]
171 fn test_handle_bar(
172 mut indicator_vwap: VolumeWeightedAveragePrice,
173 bar_ethusdt_binance_minute_bid: Bar,
174 ) {
175 indicator_vwap.handle_bar(&bar_ethusdt_binance_minute_bid);
176 assert_eq!(indicator_vwap.value, 1522.333333333333);
177 assert!(indicator_vwap.initialized);
178 }
179
180 #[rstest]
181 fn test_reset(mut indicator_vwap: VolumeWeightedAveragePrice) {
182 indicator_vwap.update_raw(10.0, 10.0, DAY0);
183 indicator_vwap.reset();
184 assert_eq!(indicator_vwap.value, 0.0);
185 assert!(!indicator_vwap.has_inputs);
186 assert!(!indicator_vwap.initialized);
187 }
188
189 #[rstest]
190 fn test_reset_on_exact_day_boundary() {
191 let mut vwap = VolumeWeightedAveragePrice::new();
192
193 vwap.update_raw(100.0, 5.0, DAY0);
194 let old = vwap.value;
195
196 vwap.update_raw(200.0, 5.0, DAY1);
197 assert_eq!(vwap.value, 200.0);
198 assert_ne!(vwap.value, old);
199 }
200
201 #[rstest]
202 fn test_no_reset_within_same_day() {
203 let mut vwap = VolumeWeightedAveragePrice::new();
204 vwap.update_raw(100.0, 5.0, DAY0);
205
206 vwap.update_raw(200.0, 5.0, DAY0 + 1.0);
207 assert!(vwap.value > 100.0 && vwap.value < 200.0);
208 }
209
210 #[rstest]
211 fn test_zero_volume_does_not_change_value() {
212 let mut vwap = VolumeWeightedAveragePrice::new();
213 vwap.update_raw(100.0, 10.0, DAY0);
214 let before = vwap.value;
215
216 vwap.update_raw(9999.0, 0.0, DAY0);
217 assert_eq!(vwap.value, before);
218 }
219
220 #[rstest]
221 fn test_epoch_day_floor_rounding() {
222 let mut vwap = VolumeWeightedAveragePrice::new();
223
224 vwap.update_raw(50.0, 5.0, DAY1 - 0.000_001);
225 let before = vwap.value;
226
227 vwap.update_raw(150.0, 5.0, DAY1);
228 assert_eq!(vwap.value, 150.0);
229 assert_ne!(vwap.value, before);
230 }
231
232 #[rstest]
233 fn test_reset_when_timestamp_goes_backwards() {
234 let mut vwap = VolumeWeightedAveragePrice::new();
235 vwap.update_raw(10.0, 10.0, DAY0);
236 vwap.update_raw(20.0, 10.0, DAY1);
237 vwap.update_raw(30.0, 10.0, DAY0);
238 assert_eq!(vwap.value, 30.0);
239 }
240
241 #[rstest]
242 #[case(10.0, 11.0)]
243 #[case(43_200.123, 86_399.999)]
244 fn test_no_reset_for_same_epoch_day(#[case] t1: f64, #[case] t2: f64) {
245 let mut vwap = VolumeWeightedAveragePrice::new();
246
247 vwap.update_raw(100.0, 10.0, t1);
248 let before = vwap.value;
249
250 vwap.update_raw(200.0, 10.0, t2);
251
252 assert!(vwap.value > before && vwap.value < 200.0);
253 }
254
255 #[rstest]
256 #[case(86_399.999, 86_400.0)]
257 #[case(86_400.0, 172_800.0)]
258 fn test_reset_when_epoch_day_changes(#[case] t1: f64, #[case] t2: f64) {
259 let mut vwap = VolumeWeightedAveragePrice::new();
260
261 vwap.update_raw(100.0, 10.0, t1);
262
263 vwap.update_raw(200.0, 10.0, t2);
264
265 assert_eq!(vwap.value, 200.0);
266 }
267
268 #[rstest]
269 fn test_first_input_zero_volume_does_not_divide_by_zero() {
270 let mut vwap = VolumeWeightedAveragePrice::new();
271
272 vwap.update_raw(100.0, 0.0, DAY0);
273 assert_eq!(vwap.value, 100.0);
274 assert!(vwap.initialized());
275
276 vwap.update_raw(200.0, 10.0, DAY0 + 1.0);
277 assert_eq!(vwap.value, 200.0);
278 }
279
280 #[rstest]
281 fn test_zero_volume_day_rollover_resets_and_seeds() {
282 let mut vwap = VolumeWeightedAveragePrice::new();
283 vwap.update_raw(100.0, 10.0, DAY0);
284
285 vwap.update_raw(9999.0, 0.0, DAY1);
286 assert_eq!(vwap.value, 9999.0);
287 }
288
289 #[rstest]
290 fn test_handle_bar_matches_update_raw(
291 mut indicator_vwap: VolumeWeightedAveragePrice,
292 bar_ethusdt_binance_minute_bid: nautilus_model::data::Bar,
293 ) {
294 indicator_vwap.handle_bar(&bar_ethusdt_binance_minute_bid);
295
296 let tp = (bar_ethusdt_binance_minute_bid.close.as_f64()
297 + bar_ethusdt_binance_minute_bid.high.as_f64()
298 + bar_ethusdt_binance_minute_bid.low.as_f64())
299 / 3.0;
300
301 let mut vwap_raw = VolumeWeightedAveragePrice::new();
302 vwap_raw.update_raw(
303 tp,
304 (&bar_ethusdt_binance_minute_bid.volume).into(),
305 bar_ethusdt_binance_minute_bid.ts_init.as_f64(),
306 );
307
308 assert!((indicator_vwap.value - vwap_raw.value).abs() < 1e-12);
309 }
310
311 #[rstest]
312 #[case(1.0e-9, 1.0e-9)]
313 #[case(1.0e9, 1.0e6)]
314 #[case(42.4242, 3.1415)]
315 fn test_extreme_prices_and_volumes_do_not_overflow(#[case] price: f64, #[case] volume: f64) {
316 let mut vwap = VolumeWeightedAveragePrice::new();
317 vwap.update_raw(price, volume, DAY0);
318 assert_eq!(vwap.value, price);
319 }
320
321 #[rstest]
322 fn negative_timestamp() {
323 let mut vwap = VolumeWeightedAveragePrice::new();
324 vwap.update_raw(42.0, 1.0, -1.0);
325 assert_eq!(vwap.value, 42.0);
326 vwap.update_raw(43.0, 1.0, -1.0);
327 assert!(vwap.value > 42.0 && vwap.value < 43.0);
328 }
329
330 #[rstest]
331 fn huge_future_timestamp_saturates() {
332 let ts = 1.0e20;
333 let mut vwap = VolumeWeightedAveragePrice::new();
334 vwap.update_raw(1.0, 1.0, ts);
335 vwap.update_raw(2.0, 1.0, ts + 1.0);
336 assert!(vwap.value > 1.0 && vwap.value < 2.0);
337 }
338
339 #[rstest]
340 fn negative_volume_changes_sign() {
341 let mut vwap = VolumeWeightedAveragePrice::new();
342 vwap.update_raw(100.0, 10.0, 0.0);
343 vwap.update_raw(200.0, -10.0, 0.0);
344 assert_eq!(vwap.volume_total, 0.0);
345 }
346
347 #[rstest]
348 fn nan_volume_propagates() {
349 let mut vwap = VolumeWeightedAveragePrice::new();
350 vwap.update_raw(100.0, 1.0, 0.0);
351 vwap.update_raw(200.0, f64::NAN, 0.0);
352 assert!(vwap.value.is_nan());
353 }
354
355 #[rstest]
356 fn zero_and_negative_price() {
357 let mut vwap = VolumeWeightedAveragePrice::new();
358 vwap.update_raw(0.0, 5.0, 0.0);
359 assert_eq!(vwap.value, 0.0);
360 vwap.update_raw(-10.0, 5.0, 0.0);
361 assert!(vwap.value < 0.0);
362 }
363
364 #[rstest]
365 fn nan_price_propagates() {
366 let mut vwap = VolumeWeightedAveragePrice::new();
367 vwap.update_raw(f64::NAN, 1.0, 0.0);
368 assert!(vwap.value.is_nan());
369 }
370}