Skip to main content

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}