nautilus_indicators/momentum/
kvo.rs1use std::fmt::{Debug, Display};
17
18use nautilus_model::data::Bar;
19
20use crate::{
21 average::{MovingAverageFactory, MovingAverageType},
22 indicator::{Indicator, MovingAverage},
23};
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators", unsendable)
30)]
31#[cfg_attr(
32 feature = "python",
33 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
34)]
35pub struct KlingerVolumeOscillator {
36 pub fast_period: usize,
37 pub slow_period: usize,
38 pub signal_period: usize,
39 pub ma_type: MovingAverageType,
40 pub value: f64,
41 pub initialized: bool,
42 fast_ma: Box<dyn MovingAverage + Send + 'static>,
43 slow_ma: Box<dyn MovingAverage + Send + 'static>,
44 signal_ma: Box<dyn MovingAverage + Send + 'static>,
45 has_inputs: bool,
46 hlc3: f64,
47 previous_hlc3: f64,
48}
49
50impl Display for KlingerVolumeOscillator {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(
53 f,
54 "{}({},{},{},{})",
55 self.name(),
56 self.fast_period,
57 self.slow_period,
58 self.signal_period,
59 self.ma_type,
60 )
61 }
62}
63
64impl Indicator for KlingerVolumeOscillator {
65 fn name(&self) -> String {
66 stringify!(KlingerVolumeOscillator).to_string()
67 }
68
69 fn has_inputs(&self) -> bool {
70 self.has_inputs
71 }
72
73 fn initialized(&self) -> bool {
74 self.initialized
75 }
76
77 fn handle_bar(&mut self, bar: &Bar) {
78 self.update_raw(
79 (&bar.high).into(),
80 (&bar.low).into(),
81 (&bar.close).into(),
82 (&bar.volume).into(),
83 );
84 }
85
86 fn reset(&mut self) {
87 self.hlc3 = 0.0;
88 self.previous_hlc3 = 0.0;
89 self.fast_ma.reset();
90 self.slow_ma.reset();
91 self.signal_ma.reset();
92 self.value = 0.0;
93 self.has_inputs = false;
94 self.initialized = false;
95 }
96}
97
98impl KlingerVolumeOscillator {
99 #[must_use]
101 pub fn new(
102 fast_period: usize,
103 slow_period: usize,
104 signal_period: usize,
105 ma_type: Option<MovingAverageType>,
106 ) -> Self {
107 Self {
108 fast_period,
109 slow_period,
110 signal_period,
111 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
112 value: 0.0,
113 fast_ma: MovingAverageFactory::create(
114 ma_type.unwrap_or(MovingAverageType::Simple),
115 fast_period,
116 ),
117 slow_ma: MovingAverageFactory::create(
118 ma_type.unwrap_or(MovingAverageType::Simple),
119 slow_period,
120 ),
121 signal_ma: MovingAverageFactory::create(
122 ma_type.unwrap_or(MovingAverageType::Simple),
123 signal_period,
124 ),
125 has_inputs: false,
126 hlc3: 0.0,
127 previous_hlc3: 0.0,
128 initialized: false,
129 }
130 }
131
132 pub fn update_raw(&mut self, high: f64, low: f64, close: f64, volume: f64) {
133 self.hlc3 = (high + low + close) / 3.0;
134 if self.hlc3 > self.previous_hlc3 {
135 self.fast_ma.update_raw(volume);
136 self.slow_ma.update_raw(volume);
137 } else if self.hlc3 < self.previous_hlc3 {
138 self.fast_ma.update_raw(-volume);
139 self.slow_ma.update_raw(-volume);
140 } else {
141 self.fast_ma.update_raw(0.0);
142 self.slow_ma.update_raw(0.0);
143 }
144
145 if self.slow_ma.initialized() {
146 self.signal_ma
147 .update_raw(self.fast_ma.value() - self.slow_ma.value());
148 self.value = self.signal_ma.value();
149 }
150
151 if !self.initialized {
153 self.has_inputs = true;
154
155 if self.signal_ma.initialized() {
156 self.initialized = true;
157 }
158 }
159
160 self.previous_hlc3 = self.hlc3;
161 }
162
163 pub fn _check_initialized(&mut self) {
164 if !self.initialized {
165 self.has_inputs = true;
166
167 if self.signal_ma.initialized() {
168 self.initialized = true;
169 }
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use rstest::rstest;
177
178 use super::*;
179 use crate::stubs::kvo_345;
180
181 #[rstest]
182 fn test_name_returns_expected_string(kvo_345: KlingerVolumeOscillator) {
183 assert_eq!(kvo_345.name(), "KlingerVolumeOscillator");
184 }
185
186 #[rstest]
187 fn test_str_repr_returns_expected_string(kvo_345: KlingerVolumeOscillator) {
188 assert_eq!(
189 format!("{kvo_345}"),
190 "KlingerVolumeOscillator(3,4,5,SIMPLE)"
191 );
192 }
193
194 #[rstest]
195 fn test_period_returns_expected_value(kvo_345: KlingerVolumeOscillator) {
196 assert_eq!(kvo_345.fast_period, 3);
197 assert_eq!(kvo_345.slow_period, 4);
198 assert_eq!(kvo_345.signal_period, 5);
199 }
200
201 #[rstest]
202 fn test_initialized_without_inputs_returns_false(kvo_345: KlingerVolumeOscillator) {
203 assert!(!kvo_345.initialized());
204 }
205
206 #[rstest]
207 fn test_value_with_all_higher_inputs_returns_expected_value(
208 mut kvo_345: KlingerVolumeOscillator,
209 ) {
210 let high_values = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
211 let low_values = [0.9, 1.9, 2.9, 3.9, 4.9, 5.9, 6.9, 7.9, 8.9, 9.9];
212 let close_values = [1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 8.1, 9.1, 10.1];
213 let volume_values = [
214 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0,
215 ];
216
217 for i in 0..10 {
218 kvo_345.update_raw(
219 high_values[i],
220 low_values[i],
221 close_values[i],
222 volume_values[i],
223 );
224 }
225
226 assert!(kvo_345.initialized());
227 assert_eq!(kvo_345.value, 50.0);
228 }
229
230 #[rstest]
231 fn test_reset_successfully_returns_indicator_to_fresh_state(
232 mut kvo_345: KlingerVolumeOscillator,
233 ) {
234 kvo_345.update_raw(1.00020, 1.00030, 1.00040, 1.00050);
235 kvo_345.update_raw(1.00030, 1.00040, 1.00050, 1.00060);
236 kvo_345.update_raw(1.00050, 1.00060, 1.00070, 1.00080);
237
238 kvo_345.reset();
239
240 assert!(!kvo_345.initialized());
241 assert_eq!(kvo_345.value, 0.0);
242 assert_eq!(kvo_345.hlc3, 0.0);
243 assert_eq!(kvo_345.previous_hlc3, 0.0);
244 }
245}