nautilus_indicators/momentum/
swings.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_model::data::Bar;
22
23use crate::indicator::Indicator;
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
30)]
31pub struct Swings {
32 pub period: usize,
33 pub direction: i64,
34 pub changed: bool,
35 pub high_datetime: f64,
36 pub low_datetime: f64,
37 pub high_price: f64,
38 pub low_price: f64,
39 pub length: usize,
40 pub duration: usize,
41 pub since_high: usize,
42 pub since_low: usize,
43 high_inputs: VecDeque<f64>,
44 low_inputs: VecDeque<f64>,
45 has_inputs: bool,
46 initialized: bool,
47}
48
49impl Display for Swings {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "{}({})", self.name(), self.period,)
52 }
53}
54
55impl Indicator for Swings {
56 fn name(&self) -> String {
57 stringify!(Swings).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.high).into(), (&bar.low).into(), bar.ts_init.as_f64());
70 }
71
72 fn reset(&mut self) {
73 self.high_inputs.clear();
74 self.low_inputs.clear();
75 self.has_inputs = false;
76 self.initialized = false;
77 self.direction = 0;
78 self.changed = false;
79 self.high_datetime = 0.0;
80 self.low_datetime = 0.0;
81 self.high_price = 0.0;
82 self.low_price = 0.0;
83 self.length = 0;
84 self.duration = 0;
85 self.since_high = 0;
86 self.since_low = 0;
87 }
88}
89
90impl Swings {
91 #[must_use]
93 pub fn new(period: usize) -> Self {
94 Self {
95 period,
96 high_inputs: VecDeque::with_capacity(period + 1),
97 low_inputs: VecDeque::with_capacity(period + 1),
98 has_inputs: false,
99 initialized: false,
100 direction: 0,
101 changed: false,
102 high_datetime: 0.0,
103 low_datetime: 0.0,
104 high_price: 0.0,
105 low_price: 0.0,
106 length: 0,
107 duration: 0,
108 since_high: 0,
109 since_low: 0,
110 }
111 }
112
113 pub fn update_raw(&mut self, high: f64, low: f64, timestamp: f64) {
114 self.high_inputs.push_back(high);
116 self.low_inputs.push_back(low);
117
118 let max_high = self.high_inputs.iter().fold(f64::MIN, |a, &b| a.max(b));
120 let min_low = self.low_inputs.iter().fold(f64::MAX, |a, &b| a.min(b));
121
122 let is_swing_high = high >= max_high && low >= min_low;
124 let is_swing_low = high <= max_high && low <= min_low;
125
126 self.changed = true;
128
129 if is_swing_high && !is_swing_low {
130 if self.direction == -1 {
131 self.changed = true;
132 }
133 self.high_price = high;
134 self.high_datetime = timestamp;
135 self.direction = 1;
136 self.since_high = 0;
137 self.since_low += 1;
138 } else if is_swing_low && !is_swing_high {
139 if self.direction == 1 {
140 self.changed = true;
141 }
142 self.low_price = low;
143 self.low_datetime = timestamp;
144 self.direction = -1;
145 self.since_high += 1;
146 self.since_low = 0;
147 } else {
148 self.since_high += 1;
149 self.since_low += 1;
150 }
151
152 if self.initialized {
154 self.length = (self.high_price - self.low_price) as usize;
155 if self.direction == 1 {
156 self.duration = self.since_low;
157 } else {
158 self.duration = self.since_high;
159 }
160 } else {
161 self.has_inputs = true;
162 if self.high_price != 0.0 && self.low_price != 0.0 {
163 self.initialized = true;
164 }
165 }
166 }
167}
168
169#[cfg(test)]
173mod tests {
174 use rstest::rstest;
175
176 use super::*;
177 use crate::stubs::swings_10;
178
179 #[rstest]
180 fn test_name_returns_expected_string(swings_10: Swings) {
181 assert_eq!(swings_10.name(), "Swings");
182 }
183
184 #[rstest]
185 fn test_str_repr_returns_expected_string(swings_10: Swings) {
186 assert_eq!(format!("{swings_10}"), "Swings(10)");
187 }
188
189 #[rstest]
190 fn test_period_returns_expected_value(swings_10: Swings) {
191 assert_eq!(swings_10.period, 10);
192 }
193
194 #[rstest]
195 fn test_initialized_without_inputs_returns_false(swings_10: Swings) {
196 assert!(!swings_10.initialized());
197 }
198
199 #[rstest]
200 fn test_value_with_all_higher_inputs_returns_expected_value(mut swings_10: Swings) {
201 let high = [
202 0.9, 1.9, 2.9, 3.9, 4.9, 3.2, 6.9, 7.9, 8.9, 9.9, 1.1, 3.2, 10.3, 11.1, 11.4,
203 ];
204 let low = [
205 0.8, 1.8, 2.8, 3.8, 4.8, 3.1, 6.8, 7.8, 0.8, 9.8, 1.0, 3.1, 10.2, 11.0, 11.3,
206 ];
207 let time = [
208 1_643_723_400.0,
209 1_643_723_410.0,
210 1_643_723_420.0,
211 1_643_723_430.0,
212 1_643_723_440.0,
213 1_643_723_450.0,
214 1_643_723_460.0,
215 1_643_723_470.0,
216 1_643_723_480.0,
217 1_643_723_490.0,
218 1_643_723_500.0,
219 1_643_723_510.0,
220 1_643_723_520.0,
221 1_643_723_530.0,
222 1_643_723_540.0,
223 ];
224
225 for i in 0..15 {
226 swings_10.update_raw(high[i], low[i], time[i]);
227 }
228
229 assert_eq!(swings_10.direction, 1);
230 assert_eq!(swings_10.high_price, 11.4);
231 assert_eq!(swings_10.low_price, 0.0);
232 assert_eq!(swings_10.high_datetime, time[14]);
233 assert_eq!(swings_10.low_datetime, 0.0);
234 assert_eq!(swings_10.length, 0);
235 assert_eq!(swings_10.duration, 0);
236 assert_eq!(swings_10.since_high, 0);
237 assert_eq!(swings_10.since_low, 15);
238 }
239
240 #[rstest]
241 fn test_reset_successfully_returns_indicator_to_fresh_state(mut swings_10: Swings) {
242 let high = [1.0, 2.0, 3.0, 4.0, 5.0];
244 let low = [0.9, 1.9, 2.9, 3.9, 4.9];
245 let time = [
246 1_643_723_400.0,
247 1_643_723_410.0,
248 1_643_723_420.0,
249 1_643_723_430.0,
250 1_643_723_440.0,
251 ];
252
253 for i in 0..5 {
254 swings_10.update_raw(high[i], low[i], time[i]);
255 }
256
257 swings_10.reset();
258
259 assert!(!swings_10.initialized());
260 assert_eq!(swings_10.direction, 0);
261 assert_eq!(swings_10.high_price, 0.0);
262 assert_eq!(swings_10.low_price, 0.0);
263 assert_eq!(swings_10.high_datetime, 0.0);
264 assert_eq!(swings_10.low_datetime, 0.0);
265 assert_eq!(swings_10.length, 0);
266 assert_eq!(swings_10.duration, 0);
267 assert_eq!(swings_10.since_high, 0);
268 assert_eq!(swings_10.since_low, 0);
269 assert!(swings_10.high_inputs.is_empty());
270 assert!(swings_10.low_inputs.is_empty());
271 }
272}