nautilus_indicators/python/momentum/kvo.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 nautilus_core::python::to_pyvalue_err;
17use nautilus_model::data::Bar;
18use pyo3::prelude::*;
19
20use crate::{
21 average::MovingAverageType, indicator::Indicator, momentum::kvo::KlingerVolumeOscillator,
22 python::float_precision,
23};
24
25#[pymethods]
26#[pyo3_stub_gen::derive::gen_stub_pymethods]
27impl KlingerVolumeOscillator {
28 /// Stephen J. Klinger's Volume Oscillator: a fast/slow moving-average
29 /// difference over the per-bar "volume force".
30 ///
31 /// ```text
32 /// dm_t = high_t - low_t (the daily measurement)
33 /// trend = sign(hlc_t - hlc_{t-1}), carried over when equal, with hlc = high + low + close
34 /// cm_t = cm_{t-1} + dm_t while the trend holds
35 /// cm_t = dm_{t-1} + dm_t when the trend flips
36 /// vf_t = volume_t * |2 * (dm_t / cm_t - 1)| * trend * 100
37 /// KVO_t = MA(vf, fast) - MA(vf, slow)
38 /// ```
39 ///
40 /// Klinger's textbook configuration is `fast = 34, slow = 55` with exponential
41 /// averages, which is the default `ma_type`.
42 #[new]
43 #[pyo3(signature = (fast_period, slow_period, ma_type=None))]
44 pub fn py_new(
45 fast_period: usize,
46 slow_period: usize,
47 ma_type: Option<MovingAverageType>,
48 ) -> PyResult<Self> {
49 Self::new_checked(fast_period, slow_period, ma_type).map_err(to_pyvalue_err)
50 }
51
52 fn __repr__(&self) -> String {
53 format!(
54 "KlingerVolumeOscillator({},{},{})",
55 self.fast_period, self.slow_period, self.ma_type
56 )
57 }
58
59 #[getter]
60 #[pyo3(name = "name")]
61 fn py_name(&self) -> String {
62 self.name()
63 }
64
65 #[getter]
66 #[pyo3(name = "fast_period")]
67 const fn py_fast_period(&self) -> usize {
68 self.fast_period
69 }
70
71 #[getter]
72 #[pyo3(name = "slow_period")]
73 const fn py_slow_period(&self) -> usize {
74 self.slow_period
75 }
76
77 #[getter]
78 #[pyo3(name = "has_inputs")]
79 fn py_has_inputs(&self) -> bool {
80 self.has_inputs()
81 }
82
83 #[getter]
84 #[pyo3(name = "value")]
85 const fn py_value(&self) -> f64 {
86 self.value
87 }
88
89 #[getter]
90 #[pyo3(name = "initialized")]
91 const fn py_initialized(&self) -> bool {
92 self.initialized
93 }
94
95 #[pyo3(name = "update_raw")]
96 fn py_update_raw(&mut self, high: f64, low: f64, close: f64, volume: f64) {
97 self.update_raw(high, low, close, volume);
98 }
99
100 #[pyo3(name = "handle_bar")]
101 fn py_handle_bar(&mut self, bar: &Bar) -> PyResult<()> {
102 float_precision::check_bar(bar)?;
103 float_precision::check_bar_volume(bar)?;
104 self.handle_bar(bar);
105 Ok(())
106 }
107
108 #[pyo3(name = "reset")]
109 fn py_reset(&mut self) {
110 self.reset();
111 }
112}