1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_core::correctness::FAILED;
20use nautilus_model::data::{Bar, QuoteTick, TradeTick};
21
22use crate::{
23 average::{MovingAverageFactory, MovingAverageType},
24 indicator::{Indicator, MovingAverage},
25 support::MAX_PERIOD,
26};
27
28const DEFAULT_MA_TYPE: MovingAverageType = MovingAverageType::Exponential;
29const MAX_SIGNAL: usize = 1_024;
30
31type SignalBuf = ArrayDeque<f64, { MAX_SIGNAL + 1 }, Wrapping>;
32
33#[repr(C)]
34#[derive(Debug)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
42)]
43pub struct ArcherMovingAveragesTrends {
44 pub fast_period: usize,
45 pub slow_period: usize,
46 pub signal_period: usize,
47 pub ma_type: MovingAverageType,
48 pub long_run: bool,
49 pub short_run: bool,
50 pub initialized: bool,
51 fast_ma: Box<dyn MovingAverage + Send + 'static>,
52 slow_ma: Box<dyn MovingAverage + Send + 'static>,
53 fast_ma_price: SignalBuf,
54 slow_ma_price: SignalBuf,
55 has_inputs: bool,
56}
57
58impl Display for ArcherMovingAveragesTrends {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(
61 f,
62 "{}({},{},{},{})",
63 self.name(),
64 self.fast_period,
65 self.slow_period,
66 self.signal_period,
67 self.ma_type,
68 )
69 }
70}
71
72impl Indicator for ArcherMovingAveragesTrends {
73 fn name(&self) -> String {
74 stringify!(ArcherMovingAveragesTrends).into()
75 }
76
77 fn has_inputs(&self) -> bool {
78 self.has_inputs
79 }
80
81 fn initialized(&self) -> bool {
82 self.initialized
83 }
84
85 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
86 Ok(())
87 }
88
89 fn handle_trade(&mut self, _trade: &TradeTick) {}
90
91 fn handle_bar(&mut self, bar: &Bar) {
92 self.update_raw(bar.close.into());
93 }
94
95 fn reset(&mut self) {
96 self.fast_ma.reset();
97 self.slow_ma.reset();
98 self.long_run = false;
99 self.short_run = false;
100 self.fast_ma_price.clear();
101 self.slow_ma_price.clear();
102 self.has_inputs = false;
103 self.initialized = false;
104 }
105}
106
107impl ArcherMovingAveragesTrends {
108 #[must_use]
118 pub fn new(
119 fast_period: usize,
120 slow_period: usize,
121 signal_period: usize,
122 ma_type: Option<MovingAverageType>,
123 ) -> Self {
124 Self::new_checked(fast_period, slow_period, signal_period, ma_type).expect(FAILED)
125 }
126
127 pub(crate) fn new_checked(
128 fast_period: usize,
129 slow_period: usize,
130 signal_period: usize,
131 ma_type: Option<MovingAverageType>,
132 ) -> anyhow::Result<Self> {
133 anyhow::ensure!(
134 fast_period > 0,
135 "fast_period must be positive (received {fast_period})"
136 );
137 anyhow::ensure!(
138 slow_period > 0,
139 "slow_period must be positive (received {slow_period})"
140 );
141 anyhow::ensure!(
142 signal_period > 0,
143 "signal_period must be positive (received {signal_period})"
144 );
145 anyhow::ensure!(
146 slow_period > fast_period,
147 "slow_period ({slow_period}) must be greater than fast_period ({fast_period})"
148 );
149 anyhow::ensure!(
150 signal_period <= MAX_SIGNAL,
151 "signal_period ({signal_period}) must not exceed MAX_SIGNAL ({MAX_SIGNAL})"
152 );
153
154 anyhow::ensure!(
155 slow_period <= MAX_PERIOD,
156 "slow_period cannot exceed {MAX_PERIOD}"
157 );
158
159 let ma_type = ma_type.unwrap_or(DEFAULT_MA_TYPE);
160
161 Ok(Self {
162 fast_period,
163 slow_period,
164 signal_period,
165 ma_type,
166 long_run: false,
167 short_run: false,
168 fast_ma: MovingAverageFactory::create(ma_type, fast_period),
169 slow_ma: MovingAverageFactory::create(ma_type, slow_period),
170 fast_ma_price: SignalBuf::new(),
171 slow_ma_price: SignalBuf::new(),
172 has_inputs: false,
173 initialized: false,
174 })
175 }
176
177 pub fn update_raw(&mut self, close: f64) {
182 self.fast_ma.update_raw(close);
183 self.slow_ma.update_raw(close);
184
185 if self.slow_ma.initialized() {
186 self.fast_ma_price.push_back(self.fast_ma.value());
187 self.slow_ma_price.push_back(self.slow_ma.value());
188
189 let max_len = self.signal_period + 1;
190 if self.fast_ma_price.len() > max_len {
191 self.fast_ma_price.pop_front();
192 self.slow_ma_price.pop_front();
193 }
194
195 let fast_back = self.fast_ma.value();
196 let fast_front = *self
197 .fast_ma_price
198 .front()
199 .expect("buffer has at least one element");
200
201 let fast_diff = fast_back - fast_front;
202 self.long_run = fast_diff > 0.0;
203 self.short_run = fast_diff < 0.0;
204 }
205
206 if !self.initialized {
207 self.has_inputs = true;
208 let max_len = self.signal_period + 1;
209 if self.slow_ma_price.len() == max_len && self.slow_ma.initialized() {
210 self.initialized = true;
211 }
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use rstest::rstest;
219
220 use super::*;
221 use crate::stubs::amat_345;
222
223 fn make(fast: usize, slow: usize, signal: usize) {
224 let _ = ArcherMovingAveragesTrends::new(fast, slow, signal, None);
225 }
226
227 #[rstest]
228 #[case(0, 2, 1, "fast_period must be positive (received 0)")]
229 #[case(1, 0, 1, "slow_period must be positive (received 0)")]
230 #[case(1, 2, 0, "signal_period must be positive (received 0)")]
231 #[case(2, 2, 1, "slow_period (2) must be greater than fast_period (2)")]
232 #[case(1, 2, MAX_SIGNAL + 1, "signal_period (1025) must not exceed MAX_SIGNAL (1024)")]
233 #[case(1, MAX_PERIOD + 1, 1, "slow_period cannot exceed 16777216")]
234 #[case(usize::MAX - 1, usize::MAX, 1, "slow_period cannot exceed 16777216")]
235 fn test_checked_constructor_rejects_invalid_periods(
236 #[case] fast: usize,
237 #[case] slow: usize,
238 #[case] signal: usize,
239 #[case] message: &str,
240 #[values(
241 MovingAverageType::Simple,
242 MovingAverageType::Exponential,
243 MovingAverageType::DoubleExponential,
244 MovingAverageType::Wilder,
245 MovingAverageType::Hull
246 )]
247 ma_type: MovingAverageType,
248 ) {
249 let error =
250 ArcherMovingAveragesTrends::new_checked(fast, slow, signal, Some(ma_type)).unwrap_err();
251 assert_eq!(error.to_string(), message);
252 }
253
254 #[rstest]
255 fn test_checked_constructor_accepts_maximum_periods() {
256 let ind =
257 ArcherMovingAveragesTrends::new_checked(MAX_PERIOD - 1, MAX_PERIOD, MAX_SIGNAL, None)
258 .unwrap();
259 assert_eq!(
260 (ind.fast_period, ind.slow_period, ind.signal_period),
261 (MAX_PERIOD - 1, MAX_PERIOD, MAX_SIGNAL)
262 );
263 assert_eq!(ind.ma_type, MovingAverageType::Exponential);
264 assert!(!ind.initialized());
265 assert!(!ind.has_inputs());
266 assert!(!ind.long_run);
267 assert!(!ind.short_run);
268 }
269
270 #[rstest]
271 fn default_ma_type_is_exponential() {
272 let ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
273 assert_eq!(ind.ma_type, MovingAverageType::Exponential);
274 }
275
276 #[rstest]
277 fn test_name_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
278 assert_eq!(amat_345.name(), "ArcherMovingAveragesTrends");
279 }
280
281 #[rstest]
282 fn test_str_repr_returns_expected_string(amat_345: ArcherMovingAveragesTrends) {
283 assert_eq!(
284 format!("{amat_345}"),
285 "ArcherMovingAveragesTrends(3,4,5,SIMPLE)"
286 );
287 }
288
289 #[rstest]
290 fn test_period_returns_expected_value(amat_345: ArcherMovingAveragesTrends) {
291 assert_eq!(amat_345.fast_period, 3);
292 assert_eq!(amat_345.slow_period, 4);
293 assert_eq!(amat_345.signal_period, 5);
294 }
295
296 #[rstest]
297 fn test_initialized_without_inputs_returns_false(amat_345: ArcherMovingAveragesTrends) {
298 assert!(!amat_345.initialized());
299 }
300
301 #[rstest]
302 #[should_panic(expected = "fast_period must be positive")]
303 fn new_panics_on_zero_fast_period() {
304 make(0, 4, 5);
305 }
306
307 #[rstest]
308 #[should_panic(expected = "slow_period must be positive")]
309 fn new_panics_on_zero_slow_period() {
310 make(3, 0, 5);
311 }
312
313 #[rstest]
314 #[should_panic(expected = "signal_period must be positive")]
315 fn new_panics_on_zero_signal_period() {
316 make(3, 5, 0);
317 }
318
319 #[rstest]
320 #[should_panic(expected = "slow_period (3) must be greater than fast_period (3)")]
321 fn new_panics_when_slow_not_greater_than_fast() {
322 make(3, 3, 5);
323 }
324
325 #[rstest]
326 #[should_panic(expected = "slow_period (2) must be greater than fast_period (3)")]
327 fn new_panics_when_slow_less_than_fast() {
328 make(3, 2, 5);
329 }
330
331 fn feed_sequence(ind: &mut ArcherMovingAveragesTrends, start: i64, count: usize, step: i64) {
332 (0..count).for_each(|i| ind.update_raw((start + i as i64 * step) as f64));
333 }
334
335 #[rstest]
336 fn buffer_len_never_exceeds_signal_plus_one() {
337 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
338 feed_sequence(&mut ind, 0, 100, 1);
339 assert_eq!(ind.fast_ma_price.len(), ind.signal_period + 1);
340 assert_eq!(ind.slow_ma_price.len(), ind.signal_period + 1);
341 }
342
343 #[rstest]
344 fn initialized_becomes_true_after_slow_ready_and_buffer_full() {
345 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
346 feed_sequence(&mut ind, 0, 11, 1); assert!(ind.initialized());
348 }
349
350 #[rstest]
351 fn long_run_flag_sets_on_bullish_trend() {
352 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
353 feed_sequence(&mut ind, 0, 60, 1);
354 assert!(ind.long_run, "Expected long_run=TRUE on up-trend");
355 assert!(!ind.short_run, "short_run should remain FALSE here");
356 }
357
358 #[rstest]
359 fn short_run_flag_sets_on_bearish_trend() {
360 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
361 feed_sequence(&mut ind, 100, 60, -1);
362 assert!(ind.short_run, "Expected short_run=TRUE on down-trend");
363 assert!(!ind.long_run, "long_run should remain FALSE here");
364 }
365
366 #[rstest]
367 fn long_run_clears_after_sustained_bearish_reversal() {
368 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, Some(MovingAverageType::Simple));
369
370 for value in 0..60 {
371 ind.update_raw(f64::from(value));
372 }
373
374 assert!(ind.initialized());
375 assert!(ind.long_run);
376 assert!(!ind.short_run);
377
378 for value in (20..60).rev() {
379 ind.update_raw(f64::from(value));
380 }
381
382 assert!(!ind.long_run);
383 assert!(ind.short_run);
384 }
385
386 #[rstest]
387 fn short_run_clears_after_sustained_bullish_reversal() {
388 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, Some(MovingAverageType::Simple));
389
390 for value in (0..60).rev() {
391 ind.update_raw(f64::from(value));
392 }
393
394 assert!(ind.initialized());
395 assert!(ind.short_run);
396 assert!(!ind.long_run);
397
398 for value in 20..60 {
399 ind.update_raw(f64::from(value));
400 }
401
402 assert!(ind.long_run);
403 assert!(!ind.short_run);
404 }
405
406 #[rstest]
407 fn reset_clears_internal_state() {
408 let mut ind = ArcherMovingAveragesTrends::new(3, 4, 5, None);
409 feed_sequence(&mut ind, 0, 50, 1);
410 assert!(ind.long_run || ind.short_run);
411 assert!(!ind.fast_ma_price.is_empty());
412
413 ind.reset();
414
415 assert!(!ind.long_run && !ind.short_run);
416 assert_eq!(ind.fast_ma_price.len(), 0);
417 assert_eq!(ind.slow_ma_price.len(), 0);
418 assert!(!ind.initialized());
419 assert!(!ind.has_inputs());
420 }
421
422 #[rstest]
423 #[should_panic(expected = "signal_period (1025) must not exceed MAX_SIGNAL (1024)")]
424 fn new_panics_when_signal_exceeds_max() {
425 let _ = ArcherMovingAveragesTrends::new(3, 4, MAX_SIGNAL + 1, None);
426 }
427
428 #[rstest]
429 fn ma_type_override_is_respected() {
430 let ind = ArcherMovingAveragesTrends::new(3, 4, 5, Some(MovingAverageType::Simple));
431 assert_eq!(ind.ma_type, MovingAverageType::Simple);
432 }
433}