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 if self.prices.len() == self.period {
117 let _ = self.prices.pop_front();
118 }
119
120 let _ = self.prices.push_back(close);
121
122 let max_price = self
123 .prices
124 .iter()
125 .copied()
126 .fold(f64::NEG_INFINITY, f64::max);
127
128 let min_price = self.prices.iter().copied().fold(f64::INFINITY, f64::min);
129
130 self.ma.update_raw(f64::abs(close - self.previous_close));
131
132 if self.initialized {
133 self.value = f64::abs(max_price - min_price) / self.period as f64 / self.ma.value();
134 }
135
136 self.previous_close = close;
137 self.check_initialized();
138 }
139
140 pub fn check_initialized(&mut self) {
141 if !self.initialized {
142 self.has_inputs = true;
143
144 if self.ma.initialized() {
145 self.initialized = true;
146 }
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use nautilus_model::data::Bar;
154 use rstest::rstest;
155
156 use crate::{indicator::Indicator, momentum::vhf::VerticalHorizontalFilter, stubs::*};
157
158 #[rstest]
159 fn test_dema_initialized(vhf_10: VerticalHorizontalFilter) {
160 let display_str = format!("{vhf_10}");
161 assert_eq!(display_str, "VerticalHorizontalFilter(10,SIMPLE)");
162 assert_eq!(vhf_10.period, 10);
163 assert!(!vhf_10.initialized);
164 assert!(!vhf_10.has_inputs);
165 }
166
167 #[rstest]
168 fn test_value_with_one_input(mut vhf_10: VerticalHorizontalFilter) {
169 vhf_10.update_raw(1.0);
170 assert_eq!(vhf_10.value, 0.0);
171 }
172
173 #[rstest]
174 fn test_value_with_three_inputs(mut vhf_10: VerticalHorizontalFilter) {
175 vhf_10.update_raw(1.0);
176 vhf_10.update_raw(2.0);
177 vhf_10.update_raw(3.0);
178 assert_eq!(vhf_10.value, 0.0);
179 }
180
181 #[rstest]
182 fn test_value_with_ten_inputs(mut vhf_10: VerticalHorizontalFilter) {
183 vhf_10.update_raw(1.00000);
184 vhf_10.update_raw(1.00010);
185 vhf_10.update_raw(1.00020);
186 vhf_10.update_raw(1.00030);
187 vhf_10.update_raw(1.00040);
188 vhf_10.update_raw(1.00050);
189 vhf_10.update_raw(1.00040);
190 vhf_10.update_raw(1.00030);
191 vhf_10.update_raw(1.00020);
192 vhf_10.update_raw(1.00010);
193 vhf_10.update_raw(1.00000);
194 assert_eq!(vhf_10.value, 0.5);
195 }
196
197 #[rstest]
198 fn test_initialized_with_required_input(mut vhf_10: VerticalHorizontalFilter) {
199 for i in 1..10 {
200 vhf_10.update_raw(f64::from(i));
201 }
202 assert!(!vhf_10.initialized);
203 vhf_10.update_raw(10.0);
204 assert!(vhf_10.initialized);
205 }
206
207 #[rstest]
208 fn test_handle_bar(mut vhf_10: VerticalHorizontalFilter, bar_ethusdt_binance_minute_bid: Bar) {
209 vhf_10.handle_bar(&bar_ethusdt_binance_minute_bid);
210 assert_eq!(vhf_10.value, 0.0);
211 assert!(vhf_10.has_inputs);
212 assert!(!vhf_10.initialized);
213 }
214
215 #[rstest]
216 fn test_reset(mut vhf_10: VerticalHorizontalFilter) {
217 vhf_10.update_raw(1.0);
218 assert_eq!(vhf_10.prices.len(), 1);
219 vhf_10.reset();
220 assert_eq!(vhf_10.value, 0.0);
221 assert_eq!(vhf_10.prices.len(), 0);
222 assert!(!vhf_10.has_inputs);
223 assert!(!vhf_10.initialized);
224 }
225
226 #[rstest]
227 fn test_value_respects_period_window() {
228 let mut vhf = VerticalHorizontalFilter::new(3, None);
229
230 vhf.update_raw(100.0); vhf.update_raw(1.0);
232 vhf.update_raw(2.0);
233 vhf.update_raw(3.0);
234 vhf.update_raw(4.0);
235
236 assert_eq!(vhf.value, 2.0 / 3.0);
239 }
240}