Skip to main content

nautilus_indicators/momentum/
obv.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::indicator::Indicator;
21
22/// On-Balance Volume: Granville's cumulative signed-volume series.
23///
24/// Each bar adds `+volume`, `-volume`, or `0` depending on whether its close
25/// is above, below, or equal to the previous close. The first bar establishes
26/// the baseline at `0`.
27#[repr(C)]
28#[derive(Debug)]
29#[cfg_attr(
30    feature = "python",
31    pyo3::pyclass(module = "nautilus_trader.indicators")
32)]
33#[cfg_attr(
34    feature = "python",
35    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
36)]
37pub struct OnBalanceVolume {
38    pub value: f64,
39    pub initialized: bool,
40    has_inputs: bool,
41    previous_close: Option<f64>,
42}
43
44impl Display for OnBalanceVolume {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}()", self.name())
47    }
48}
49
50impl Indicator for OnBalanceVolume {
51    fn name(&self) -> String {
52        stringify!(OnBalanceVolume).to_string()
53    }
54
55    fn has_inputs(&self) -> bool {
56        self.has_inputs
57    }
58
59    fn initialized(&self) -> bool {
60        self.initialized
61    }
62
63    fn handle_bar(&mut self, bar: &Bar) {
64        self.update_raw((&bar.close).into(), (&bar.volume).into());
65    }
66
67    fn reset(&mut self) {
68        self.previous_close = None;
69        self.value = 0.0;
70        self.has_inputs = false;
71        self.initialized = false;
72    }
73}
74
75impl Default for OnBalanceVolume {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl OnBalanceVolume {
82    /// Creates a new [`OnBalanceVolume`] instance.
83    #[must_use]
84    pub const fn new() -> Self {
85        Self {
86            value: 0.0,
87            previous_close: None,
88            has_inputs: false,
89            initialized: false,
90        }
91    }
92
93    pub fn update_raw(&mut self, close: f64, volume: f64) {
94        if !close.is_finite() || !volume.is_finite() || volume < 0.0 {
95            return;
96        }
97
98        if let Some(previous_close) = self.previous_close {
99            if close > previous_close {
100                self.value += volume;
101            } else if close < previous_close {
102                self.value -= volume;
103            }
104        }
105        self.previous_close = Some(close);
106        self.has_inputs = true;
107        self.initialized = true;
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use rstest::rstest;
114
115    use super::*;
116
117    #[rstest]
118    fn test_name_returns_expected_string() {
119        assert_eq!(OnBalanceVolume::new().name(), "OnBalanceVolume");
120    }
121
122    #[rstest]
123    fn test_str_repr_returns_expected_string() {
124        assert_eq!(format!("{}", OnBalanceVolume::new()), "OnBalanceVolume()");
125    }
126
127    #[rstest]
128    fn test_initialized_without_inputs_returns_false() {
129        assert!(!OnBalanceVolume::new().initialized());
130    }
131
132    #[rstest]
133    fn test_first_bar_establishes_zero_baseline() {
134        let mut obv = OnBalanceVolume::new();
135        obv.update_raw(100.0, 5_000.0);
136        assert!(obv.initialized());
137        assert_eq!(obv.value, 0.0);
138    }
139
140    #[rstest]
141    fn test_cumulative_close_versus_previous_close() {
142        let mut obv = OnBalanceVolume::new();
143        obv.update_raw(100.0, 1_000.0); // baseline
144        obv.update_raw(101.0, 2_000.0); // up: +2000
145        obv.update_raw(100.5, 500.0); // down: -500
146        obv.update_raw(100.5, 900.0); // flat: 0
147        obv.update_raw(102.0, 100.0); // up: +100
148
149        assert_eq!(obv.value, 1_600.0);
150    }
151
152    #[rstest]
153    fn test_reset_successfully_returns_indicator_to_fresh_state() {
154        let mut obv = OnBalanceVolume::new();
155        obv.update_raw(100.0, 1_000.0);
156        obv.update_raw(101.0, 2_000.0);
157
158        obv.reset();
159
160        assert!(!obv.initialized());
161        assert_eq!(obv.value, 0.0);
162        assert!(!obv.has_inputs);
163
164        // Post-reset the first bar is a baseline again
165        obv.update_raw(50.0, 4_000.0);
166        assert_eq!(obv.value, 0.0);
167    }
168}