nautilus_indicators/momentum/
amat.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::{
24    average::{MovingAverageFactory, MovingAverageType},
25    indicator::{Indicator, MovingAverage},
26};
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)]
34pub struct ArcherMovingAveragesTrends {
35    pub fast_period: usize,
36    pub slow_period: usize,
37    pub signal_period: usize,
38    pub ma_type: MovingAverageType,
39    pub long_run: bool,
40    pub short_run: bool,
41    pub initialized: bool,
42    fast_ma: Box<dyn MovingAverage + Send + 'static>,
43    slow_ma: Box<dyn MovingAverage + Send + 'static>,
44    fast_ma_price: VecDeque<f64>,
45    slow_ma_price: VecDeque<f64>,
46    has_inputs: bool,
47}
48
49impl Display for ArcherMovingAveragesTrends {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(
52            f,
53            "{}({},{},{},{})",
54            self.name(),
55            self.fast_period,
56            self.slow_period,
57            self.signal_period,
58            self.ma_type,
59        )
60    }
61}
62
63impl Indicator for ArcherMovingAveragesTrends {
64    fn name(&self) -> String {
65        stringify!(ArcherMovingAveragesTrends).to_string()
66    }
67
68    fn has_inputs(&self) -> bool {
69        self.has_inputs
70    }
71
72    fn initialized(&self) -> bool {
73        self.initialized
74    }
75
76    fn handle_bar(&mut self, bar: &Bar) {
77        self.update_raw((&bar.close).into());
78    }
79
80    fn reset(&mut self) {
81        self.fast_ma.reset();
82        self.slow_ma.reset();
83        self.long_run = false;
84        self.short_run = false;
85        self.fast_ma_price.clear();
86        self.slow_ma_price.clear();
87        self.has_inputs = false;
88        self.initialized = false;
89    }
90}
91
92impl ArcherMovingAveragesTrends {
93    /// Creates a new [`ArcherMovingAveragesTrends`] instance.
94    #[must_use]
95    pub fn new(
96        fast_period: usize,
97        slow_period: usize,
98        signal_period: usize,
99        ma_type: Option<MovingAverageType>,
100    ) -> Self {
101        Self {
102            fast_period,
103            slow_period,
104            signal_period,
105            ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
106            long_run: false,
107            short_run: false,
108            fast_ma: MovingAverageFactory::create(
109                ma_type.unwrap_or(MovingAverageType::Simple),
110                fast_period,
111            ),
112            slow_ma: MovingAverageFactory::create(
113                ma_type.unwrap_or(MovingAverageType::Simple),
114                slow_period,
115            ),
116            fast_ma_price: VecDeque::with_capacity(signal_period + 1),
117            slow_ma_price: VecDeque::with_capacity(signal_period + 1),
118            has_inputs: false,
119            initialized: false,
120        }
121    }
122
123    pub fn update_raw(&mut self, close: f64) {
124        self.fast_ma.update_raw(close);
125        self.slow_ma.update_raw(close);
126
127        if self.slow_ma.initialized() {
128            self.fast_ma_price.push_back(self.fast_ma.value());
129            self.slow_ma_price.push_back(self.slow_ma.value());
130
131            let fast_back = self.fast_ma.value();
132            let slow_back = self.slow_ma.value();
133            // TODO: Reduce unwraps
134            let fast_front = self.fast_ma_price.front().unwrap();
135            let slow_front = self.slow_ma_price.front().unwrap();
136
137            self.long_run = fast_back - fast_front > 0.0 && slow_back - slow_front < 0.0;
138
139            self.long_run =
140                fast_back - fast_front > 0.0 && slow_back - slow_front > 0.0 || self.long_run;
141
142            self.short_run = fast_back - fast_front < 0.0 && slow_back - slow_front > 0.0;
143
144            self.short_run =
145                fast_back - fast_front < 0.0 && slow_back - slow_front < 0.0 || self.short_run;
146        }
147
148        // Initialization logic
149        if !self.initialized {
150            self.has_inputs = true;
151            if self.slow_ma_price.len() > self.signal_period && self.slow_ma.initialized() {
152                self.initialized = true;
153            }
154        }
155    }
156}
157
158////////////////////////////////////////////////////////////////////////////////
159// Tests
160////////////////////////////////////////////////////////////////////////////////
161#[cfg(test)]
162mod tests {
163    use rstest::rstest;
164
165    use super::*;
166    use crate::stubs::amat_345;
167
168    #[rstest]
169    fn test_name_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
170        assert_eq!(amat_345.name(), "ArcherMovingAveragesTrends");
171    }
172
173    #[rstest]
174    fn test_str_repr_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
175        assert_eq!(
176            format!("{amat_345}"),
177            "ArcherMovingAveragesTrends(3,4,5,SIMPLE)"
178        );
179    }
180
181    #[rstest]
182    fn test_period_returns_expected_value(amat_345: ArcherMovingAveragesTrends) {
183        assert_eq!(amat_345.fast_period, 3);
184        assert_eq!(amat_345.slow_period, 4);
185        assert_eq!(amat_345.signal_period, 5);
186    }
187
188    #[rstest]
189    fn test_initialized_without_inputs_returns_false(amat_345: ArcherMovingAveragesTrends) {
190        assert!(!amat_345.initialized());
191    }
192
193    #[rstest]
194    fn test_value_with_all_higher_inputs_returns_expected_value(
195        mut amat_345: ArcherMovingAveragesTrends,
196    ) {
197        let closes = [
198            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,
199        ];
200
201        for close in &closes {
202            amat_345.update_raw(*close);
203        }
204
205        assert!(amat_345.initialized());
206        assert!(amat_345.long_run);
207        assert!(!amat_345.short_run);
208    }
209
210    #[rstest]
211    fn test_reset_successfully_returns_indicator_to_fresh_state(
212        mut amat_345: ArcherMovingAveragesTrends,
213    ) {
214        amat_345.update_raw(1.00020);
215        amat_345.update_raw(1.00030);
216        amat_345.update_raw(1.00070);
217
218        amat_345.reset();
219
220        assert!(!amat_345.initialized());
221        assert!(!amat_345.long_run);
222        assert!(!amat_345.short_run);
223    }
224}