nautilus_indicators/momentum/
amat.rs1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::{Bar, QuoteTick, TradeTick};
20
21use crate::{
22 average::{MovingAverageFactory, MovingAverageType},
23 indicator::{Indicator, MovingAverage},
24};
25
26const DEFAULT_MA_TYPE: MovingAverageType = MovingAverageType::Exponential;
27const MAX_SIGNAL: usize = 1_024;
28
29type SignalBuf = ArrayDeque<f64, { MAX_SIGNAL + 1 }, Wrapping>;
30
31#[repr(C)]
32#[derive(Debug)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
40)]
41pub struct ArcherMovingAveragesTrends {
42 pub fast_period: usize,
43 pub slow_period: usize,
44 pub signal_period: usize,
45 pub ma_type: MovingAverageType,
46 pub long_run: bool,
47 pub short_run: bool,
48 pub initialized: bool,
49 fast_ma: Box<dyn MovingAverage + Send + 'static>,
50 slow_ma: Box<dyn MovingAverage + Send + 'static>,
51 fast_ma_price: SignalBuf,
52 slow_ma_price: SignalBuf,
53 has_inputs: bool,
54}
55
56impl Display for ArcherMovingAveragesTrends {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(
59 f,
60 "{}({},{},{},{})",
61 self.name(),
62 self.fast_period,
63 self.slow_period,
64 self.signal_period,
65 self.ma_type,
66 )
67 }
68}
69
70impl Indicator for ArcherMovingAveragesTrends {
71 fn name(&self) -> String {
72 stringify!(ArcherMovingAveragesTrends).into()
73 }
74
75 fn has_inputs(&self) -> bool {
76 self.has_inputs
77 }
78
79 fn initialized(&self) -> bool {
80 self.initialized
81 }
82
83 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
84 Ok(())
85 }
86
87 fn handle_trade(&mut self, _trade: &TradeTick) {}
88
89 fn handle_bar(&mut self, bar: &Bar) {
90 self.update_raw(bar.close.into());
91 }
92
93 fn reset(&mut self) {
94 self.fast_ma.reset();
95 self.slow_ma.reset();
96 self.long_run = false;
97 self.short_run = false;
98 self.fast_ma_price.clear();
99 self.slow_ma_price.clear();
100 self.has_inputs = false;
101 self.initialized = false;
102 }
103}
104
105impl ArcherMovingAveragesTrends {
106 #[must_use]
115 pub fn new(
116 fast_period: usize,
117 slow_period: usize,
118 signal_period: usize,
119 ma_type: Option<MovingAverageType>,
120 ) -> Self {
121 assert!(
122 fast_period > 0,
123 "fast_period must be positive (received {fast_period})"
124 );
125 assert!(
126 slow_period > 0,
127 "slow_period must be positive (received {slow_period})"
128 );
129 assert!(
130 signal_period > 0,
131 "signal_period must be positive (received {signal_period})"
132 );
133 assert!(
134 slow_period > fast_period,
135 "slow_period ({slow_period}) must be greater than fast_period ({fast_period})"
136 );
137 assert!(
138 signal_period <= MAX_SIGNAL,
139 "signal_period ({signal_period}) must not exceed MAX_SIGNAL ({MAX_SIGNAL})"
140 );
141
142 let ma_type = ma_type.unwrap_or(DEFAULT_MA_TYPE);
143
144 Self {
145 fast_period,
146 slow_period,
147 signal_period,
148 ma_type,
149 long_run: false,
150 short_run: false,
151 fast_ma: MovingAverageFactory::create(ma_type, fast_period),
152 slow_ma: MovingAverageFactory::create(ma_type, slow_period),
153 fast_ma_price: SignalBuf::new(),
154 slow_ma_price: SignalBuf::new(),
155 has_inputs: false,
156 initialized: false,
157 }
158 }
159
160 pub fn update_raw(&mut self, close: f64) {
165 self.fast_ma.update_raw(close);
166 self.slow_ma.update_raw(close);
167
168 if self.slow_ma.initialized() {
169 self.fast_ma_price.push_back(self.fast_ma.value());
170 self.slow_ma_price.push_back(self.slow_ma.value());
171
172 let max_len = self.signal_period + 1;
173 if self.fast_ma_price.len() > max_len {
174 self.fast_ma_price.pop_front();
175 self.slow_ma_price.pop_front();
176 }
177
178 let fast_back = self.fast_ma.value();
179 let fast_front = *self
180 .fast_ma_price
181 .front()
182 .expect("buffer has at least one element");
183
184 let fast_diff = fast_back - fast_front;
185 self.long_run = fast_diff > 0.0 || self.long_run;
186 self.short_run = fast_diff < 0.0 || self.short_run;
187 }
188
189 if !self.initialized {
190 self.has_inputs = true;
191 let max_len = self.signal_period + 1;
192 if self.slow_ma_price.len() == max_len && self.slow_ma.initialized() {
193 self.initialized = true;
194 }
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use rstest::rstest;
202
203 use super::*;
204 use crate::stubs::amat_345;
205
206 fn make(fast: usize, slow: usize, signal: usize) {
207 let _ = ArcherMovingAveragesTrends::new(fast, slow, signal, None);
208 }
209
210 #[rstest]
211 fn default_ma_type_is_exponential() {
212 let ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
213 assert_eq!(ind.ma_type, MovingAverageType::Exponential);
214 }
215
216 #[rstest]
217 fn test_name_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
218 assert_eq!(amat_345.name(), "ArcherMovingAveragesTrends");
219 }
220
221 #[rstest]
222 fn test_str_repr_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
223 assert_eq!(
224 format!("{amat_345}"),
225 "ArcherMovingAveragesTrends(3,4,5,SIMPLE)"
226 );
227 }
228
229 #[rstest]
230 fn test_period_returns_expected_value(amat_345: ArcherMovingAveragesTrends) {
231 assert_eq!(amat_345.fast_period, 3);
232 assert_eq!(amat_345.slow_period, 4);
233 assert_eq!(amat_345.signal_period, 5);
234 }
235
236 #[rstest]
237 fn test_initialized_without_inputs_returns_false(amat_345: ArcherMovingAveragesTrends) {
238 assert!(!amat_345.initialized());
239 }
240
241 #[rstest]
242 #[should_panic(expected = "fast_period must be positive")]
243 fn new_panics_on_zero_fast_period() {
244 make(0, 4, 5);
245 }
246
247 #[rstest]
248 #[should_panic(expected = "slow_period must be positive")]
249 fn new_panics_on_zero_slow_period() {
250 make(3, 0, 5);
251 }
252
253 #[rstest]
254 #[should_panic(expected = "signal_period must be positive")]
255 fn new_panics_on_zero_signal_period() {
256 make(3, 5, 0);
257 }
258
259 #[rstest]
260 #[should_panic(expected = "slow_period (3) must be greater than fast_period (3)")]
261 fn new_panics_when_slow_not_greater_than_fast() {
262 make(3, 3, 5);
263 }
264
265 #[rstest]
266 #[should_panic(expected = "slow_period (2) must be greater than fast_period (3)")]
267 fn new_panics_when_slow_less_than_fast() {
268 make(3, 2, 5);
269 }
270
271 fn feed_sequence(ind: &mut ArcherMovingAveragesTrends, start: i64, count: usize, step: i64) {
272 (0..count).for_each(|i| ind.update_raw((start + i as i64 * step) as f64));
273 }
274
275 #[rstest]
276 fn buffer_len_never_exceeds_signal_plus_one() {
277 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
278 feed_sequence(&mut ind, 0, 100, 1);
279 assert_eq!(ind.fast_ma_price.len(), ind.signal_period + 1);
280 assert_eq!(ind.slow_ma_price.len(), ind.signal_period + 1);
281 }
282
283 #[rstest]
284 fn initialized_becomes_true_after_slow_ready_and_buffer_full() {
285 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
286 feed_sequence(&mut ind, 0, 11, 1); assert!(ind.initialized());
288 }
289
290 #[rstest]
291 fn long_run_flag_sets_on_bullish_trend() {
292 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
293 feed_sequence(&mut ind, 0, 60, 1);
294 assert!(ind.long_run, "Expected long_run=TRUE on up-trend");
295 assert!(!ind.short_run, "short_run should remain FALSE here");
296 }
297
298 #[rstest]
299 fn short_run_flag_sets_on_bearish_trend() {
300 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
301 feed_sequence(&mut ind, 100, 60, -1);
302 assert!(ind.short_run, "Expected short_run=TRUE on down-trend");
303 assert!(!ind.long_run, "long_run should remain FALSE here");
304 }
305
306 #[rstest]
307 fn reset_clears_internal_state() {
308 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
309 feed_sequence(&mut ind, 0, 50, 1);
310 assert!(ind.long_run || ind.short_run);
311 assert!(!ind.fast_ma_price.is_empty());
312
313 ind.reset();
314
315 assert!(!ind.long_run && !ind.short_run);
316 assert_eq!(ind.fast_ma_price.len(), 0);
317 assert_eq!(ind.slow_ma_price.len(), 0);
318 assert!(!ind.initialized());
319 assert!(!ind.has_inputs());
320 }
321
322 #[rstest]
323 #[should_panic(expected = "signal_period (1025) must not exceed MAX_SIGNAL (1024)")]
324 fn new_panics_when_signal_exceeds_max() {
325 let _ = ArcherMovingAveragesTrends::new(3, 4, MAX_SIGNAL + 1, None);
326 }
327
328 #[rstest]
329 fn ma_type_override_is_respected() {
330 let ind = ArcherMovingAveragesTrends::new(3, 4, 5, Some(MovingAverageType::Simple));
331 assert_eq!(ind.ma_type, MovingAverageType::Simple);
332 }
333}