nautilus_indicators/momentum/
obv.rs1use std::fmt::Display;
17
18use nautilus_model::data::Bar;
19
20use crate::indicator::Indicator;
21
22#[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 #[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); obv.update_raw(101.0, 2_000.0); obv.update_raw(100.5, 500.0); obv.update_raw(100.5, 900.0); obv.update_raw(102.0, 100.0); 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 obv.update_raw(50.0, 4_000.0);
166 assert_eq!(obv.value, 0.0);
167 }
168}