nautilus_indicators/momentum/
swings.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 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    /// Creates a new [`Swings`] instance.
92    #[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        // Update inputs
115        self.high_inputs.push_back(high);
116        self.low_inputs.push_back(low);
117
118        // Update max high and min low
119        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        // Calculate if swings
123        let is_swing_high = high >= max_high && low >= min_low;
124        let is_swing_low = high <= max_high && low <= min_low;
125
126        // Swing logic
127        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        // Initialization logic
153        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////////////////////////////////////////////////////////////////////////////////
170// Tests
171////////////////////////////////////////////////////////////////////////////////
172#[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        // Update the indicator with some values
243        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}