nautilus_indicators/momentum/
vhf.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::Bar;
20
21use crate::{
22    average::{MovingAverageFactory, MovingAverageType},
23    indicator::{Indicator, MovingAverage},
24};
25
26const MAX_PERIOD: usize = 1_024;
27
28#[repr(C)]
29#[derive(Debug)]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.indicators", unsendable)
33)]
34pub struct VerticalHorizontalFilter {
35    pub period: usize,
36    pub ma_type: MovingAverageType,
37    pub value: f64,
38    pub initialized: bool,
39    ma: Box<dyn MovingAverage + Send + 'static>,
40    has_inputs: bool,
41    previous_close: f64,
42    prices: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
43}
44
45impl Display for VerticalHorizontalFilter {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "{}({},{})", self.name(), self.period, self.ma_type,)
48    }
49}
50
51impl Indicator for VerticalHorizontalFilter {
52    fn name(&self) -> String {
53        stringify!(VerticalHorizontalFilter).to_string()
54    }
55
56    fn has_inputs(&self) -> bool {
57        self.has_inputs
58    }
59
60    fn initialized(&self) -> bool {
61        self.initialized
62    }
63
64    fn handle_bar(&mut self, bar: &Bar) {
65        self.update_raw((&bar.close).into());
66    }
67
68    fn reset(&mut self) {
69        self.prices.clear();
70        self.ma.reset();
71        self.previous_close = 0.0;
72        self.value = 0.0;
73        self.has_inputs = false;
74        self.initialized = false;
75    }
76}
77
78impl VerticalHorizontalFilter {
79    /// Creates a new [`VerticalHorizontalFilter`] instance.
80    ///
81    /// # Panics
82    ///
83    /// This function panics if:
84    /// - `period` is less than or equal to 0.
85    /// - `period` exceeds `MAX_PERIOD`.
86    #[must_use]
87    pub fn new(period: usize, ma_type: Option<MovingAverageType>) -> Self {
88        assert!(
89            period > 0 && period <= MAX_PERIOD,
90            "VerticalHorizontalFilter: period {period} exceeds MAX_PERIOD ({MAX_PERIOD})"
91        );
92
93        let ma_kind = ma_type.unwrap_or(MovingAverageType::Simple);
94
95        Self {
96            period,
97            ma_type: ma_kind,
98            value: 0.0,
99            previous_close: 0.0,
100            ma: MovingAverageFactory::create(ma_kind, period),
101            has_inputs: false,
102            initialized: false,
103            prices: ArrayDeque::new(),
104        }
105    }
106
107    pub fn update_raw(&mut self, close: f64) {
108        if !self.has_inputs {
109            self.previous_close = close;
110        }
111
112        let _ = self.prices.push_back(close);
113
114        let max_price = self
115            .prices
116            .iter()
117            .copied()
118            .fold(f64::NEG_INFINITY, f64::max);
119
120        let min_price = self.prices.iter().copied().fold(f64::INFINITY, f64::min);
121
122        self.ma.update_raw(f64::abs(close - self.previous_close));
123
124        if self.initialized {
125            self.value = f64::abs(max_price - min_price) / self.period as f64 / self.ma.value();
126        }
127
128        self.previous_close = close;
129        self._check_initialized();
130    }
131
132    pub fn _check_initialized(&mut self) {
133        if !self.initialized {
134            self.has_inputs = true;
135            if self.ma.initialized() {
136                self.initialized = true;
137            }
138        }
139    }
140}
141
142////////////////////////////////////////////////////////////////////////////////
143// Tests
144////////////////////////////////////////////////////////////////////////////////
145#[cfg(test)]
146mod tests {
147    use nautilus_model::data::Bar;
148    use rstest::rstest;
149
150    use crate::{indicator::Indicator, momentum::vhf::VerticalHorizontalFilter, stubs::*};
151
152    #[rstest]
153    fn test_dema_initialized(vhf_10: VerticalHorizontalFilter) {
154        let display_str = format!("{vhf_10}");
155        assert_eq!(display_str, "VerticalHorizontalFilter(10,SIMPLE)");
156        assert_eq!(vhf_10.period, 10);
157        assert!(!vhf_10.initialized);
158        assert!(!vhf_10.has_inputs);
159    }
160
161    #[rstest]
162    fn test_value_with_one_input(mut vhf_10: VerticalHorizontalFilter) {
163        vhf_10.update_raw(1.0);
164        assert_eq!(vhf_10.value, 0.0);
165    }
166
167    #[rstest]
168    fn test_value_with_three_inputs(mut vhf_10: VerticalHorizontalFilter) {
169        vhf_10.update_raw(1.0);
170        vhf_10.update_raw(2.0);
171        vhf_10.update_raw(3.0);
172        assert_eq!(vhf_10.value, 0.0);
173    }
174
175    #[rstest]
176    fn test_value_with_ten_inputs(mut vhf_10: VerticalHorizontalFilter) {
177        vhf_10.update_raw(1.00000);
178        vhf_10.update_raw(1.00010);
179        vhf_10.update_raw(1.00020);
180        vhf_10.update_raw(1.00030);
181        vhf_10.update_raw(1.00040);
182        vhf_10.update_raw(1.00050);
183        vhf_10.update_raw(1.00040);
184        vhf_10.update_raw(1.00030);
185        vhf_10.update_raw(1.00020);
186        vhf_10.update_raw(1.00010);
187        vhf_10.update_raw(1.00000);
188        assert_eq!(vhf_10.value, 0.5);
189    }
190
191    #[rstest]
192    fn test_initialized_with_required_input(mut vhf_10: VerticalHorizontalFilter) {
193        for i in 1..10 {
194            vhf_10.update_raw(f64::from(i));
195        }
196        assert!(!vhf_10.initialized);
197        vhf_10.update_raw(10.0);
198        assert!(vhf_10.initialized);
199    }
200
201    #[rstest]
202    fn test_handle_bar(mut vhf_10: VerticalHorizontalFilter, bar_ethusdt_binance_minute_bid: Bar) {
203        vhf_10.handle_bar(&bar_ethusdt_binance_minute_bid);
204        assert_eq!(vhf_10.value, 0.0);
205        assert!(vhf_10.has_inputs);
206        assert!(!vhf_10.initialized);
207    }
208
209    #[rstest]
210    fn test_reset(mut vhf_10: VerticalHorizontalFilter) {
211        vhf_10.update_raw(1.0);
212        assert_eq!(vhf_10.prices.len(), 1);
213        vhf_10.reset();
214        assert_eq!(vhf_10.value, 0.0);
215        assert_eq!(vhf_10.prices.len(), 0);
216        assert!(!vhf_10.has_inputs);
217        assert!(!vhf_10.initialized);
218    }
219}