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