Skip to main content

nautilus_indicators/average/
vwap.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::fmt::Display;
17
18use nautilus_model::data::Bar;
19
20use crate::{
21    indicator::Indicator,
22    support::{is_valid_hlc, typical_price},
23};
24
25/// Volume-weighted average price.
26#[repr(C)]
27#[derive(Debug)]
28#[cfg_attr(
29    feature = "python",
30    pyo3::pyclass(module = "nautilus_trader.indicators")
31)]
32#[cfg_attr(
33    feature = "python",
34    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
35)]
36pub struct VolumeWeightedAveragePrice {
37    pub value: f64,
38    pub initialized: bool,
39    has_inputs: bool,
40    price_volume: f64,
41    volume_total: f64,
42}
43
44impl Indicator for VolumeWeightedAveragePrice {
45    fn name(&self) -> String {
46        stringify!(VolumeWeightedAveragePrice).to_string()
47    }
48
49    fn has_inputs(&self) -> bool {
50        self.has_inputs
51    }
52
53    fn initialized(&self) -> bool {
54        self.initialized
55    }
56
57    fn handle_bar(&mut self, bar: &Bar) {
58        let (high, low, close) = (bar.high.as_f64(), bar.low.as_f64(), bar.close.as_f64());
59        if !is_valid_hlc(high, low, close) {
60            return;
61        }
62        self.update_raw(typical_price(high, low, close), bar.volume.as_f64());
63    }
64
65    fn reset(&mut self) {
66        self.value = 0.0;
67        self.has_inputs = false;
68        self.initialized = false;
69        self.price_volume = 0.0;
70        self.volume_total = 0.0;
71    }
72}
73
74impl VolumeWeightedAveragePrice {
75    /// Creates a new [`VolumeWeightedAveragePrice`] instance.
76    #[must_use]
77    pub const fn new() -> Self {
78        Self {
79            value: 0.0,
80            initialized: false,
81            has_inputs: false,
82            price_volume: 0.0,
83            volume_total: 0.0,
84        }
85    }
86
87    /// Adds a price and nonnegative volume to the current manually reset window.
88    /// Non-finite inputs and negative volume leave state unchanged.
89    pub fn update_raw(&mut self, price: f64, volume: f64) {
90        if !price.is_finite() || !volume.is_finite() || volume < 0.0 {
91            return;
92        }
93        self.has_inputs = true;
94
95        if volume == 0.0 {
96            return;
97        }
98        self.price_volume += price * volume;
99        self.volume_total += volume;
100        self.value = self.price_volume / self.volume_total;
101        self.initialized = true;
102    }
103}
104
105impl Default for VolumeWeightedAveragePrice {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl Display for VolumeWeightedAveragePrice {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{}", self.name())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use nautilus_model::data::Bar;
120    use rstest::rstest;
121
122    use super::*;
123    use crate::stubs::*;
124
125    #[rstest]
126    fn test_manual_window_and_reset() {
127        let mut vwap = VolumeWeightedAveragePrice::new();
128        assert_eq!(vwap.to_string(), "VolumeWeightedAveragePrice");
129        assert_eq!(
130            (vwap.value, vwap.initialized(), vwap.has_inputs()),
131            (0.0, false, false)
132        );
133        vwap.update_raw(99.0, 0.0);
134        assert_eq!(
135            (vwap.value, vwap.initialized(), vwap.has_inputs()),
136            (0.0, false, true)
137        );
138
139        for (price, volume) in [(10.0, 1.0), (20.0, 3.0), (30.0, 6.0)] {
140            vwap.update_raw(price, volume);
141        }
142        assert_eq!(
143            (vwap.value, vwap.initialized(), vwap.has_inputs()),
144            (25.0, true, true)
145        );
146        vwap.update_raw(999.0, 0.0);
147        assert_eq!(vwap.value, 25.0);
148        vwap.reset();
149        assert_eq!(
150            (vwap.value, vwap.initialized(), vwap.has_inputs()),
151            (0.0, false, false)
152        );
153        vwap.update_raw(42.0, 7.0);
154        assert_eq!(vwap.value, 42.0);
155    }
156
157    #[rstest]
158    #[case(f64::NAN, 1.0)]
159    #[case(f64::INFINITY, 1.0)]
160    #[case(2.0, f64::NAN)]
161    #[case(2.0, f64::INFINITY)]
162    #[case(2.0, -1.0)]
163    fn test_invalid_input_is_atomic(#[case] price: f64, #[case] volume: f64) {
164        let mut vwap = VolumeWeightedAveragePrice::new();
165        vwap.update_raw(price, volume);
166        assert_eq!(
167            (vwap.value, vwap.initialized(), vwap.has_inputs()),
168            (0.0, false, false)
169        );
170        vwap.update_raw(10.0, 2.0);
171        vwap.update_raw(price, volume);
172        vwap.update_raw(20.0, 2.0);
173        assert_eq!(vwap.value, 15.0);
174    }
175
176    #[rstest]
177    fn test_bar_uses_typical_price(bar_ethusdt_binance_minute_bid: Bar) {
178        let bar = bar_ethusdt_binance_minute_bid;
179        let mut event = VolumeWeightedAveragePrice::new();
180        let mut raw = VolumeWeightedAveragePrice::new();
181        event.handle_bar(&bar);
182        raw.update_raw(
183            typical_price(bar.high.as_f64(), bar.low.as_f64(), bar.close.as_f64()),
184            bar.volume.as_f64(),
185        );
186        assert_eq!(
187            (event.value, event.initialized(), event.has_inputs()),
188            (raw.value, true, true)
189        );
190    }
191}