1use std::{collections::VecDeque, fmt::Display};
17
18use nautilus_core::correctness::{FAILED, check_predicate_true};
19use nautilus_model::{
20 data::{Bar, QuoteTick, TradeTick},
21 enums::PriceType,
22};
23
24use crate::{
25 indicator::{Indicator, MovingAverage},
26 support::{MAX_PERIOD, SMA_RESEED_WINDOWS},
27};
28
29#[repr(C)]
31#[derive(Debug)]
32#[cfg_attr(
33 feature = "python",
34 pyo3::pyclass(module = "nautilus_trader.indicators")
35)]
36#[cfg_attr(
37 feature = "python",
38 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
39)]
40pub struct SimpleMovingAverage {
41 pub period: usize,
42 pub price_type: PriceType,
43 pub value: f64,
44 sum: f64,
45 updates_since_reseed: usize,
46 pub count: usize,
47 buf: VecDeque<f64>,
48 pub initialized: bool,
49}
50
51impl Display for SimpleMovingAverage {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}({})", self.name(), self.period)
54 }
55}
56
57impl Indicator for SimpleMovingAverage {
58 fn name(&self) -> String {
59 stringify!(SimpleMovingAverage).into()
60 }
61
62 fn has_inputs(&self) -> bool {
63 self.count > 0
64 }
65
66 fn initialized(&self) -> bool {
67 self.initialized
68 }
69
70 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
71 self.process_raw(quote.extract_price(self.price_type)?.into());
72 Ok(())
73 }
74
75 fn handle_trade(&mut self, trade: &TradeTick) {
76 self.process_raw(trade.price.into());
77 }
78
79 fn handle_bar(&mut self, bar: &Bar) {
80 self.process_raw(bar.close.into());
81 }
82
83 fn reset(&mut self) {
84 self.value = 0.0;
85 self.sum = 0.0;
86 self.updates_since_reseed = 0;
87 self.count = 0;
88 self.buf.clear();
89 self.initialized = false;
90 }
91}
92
93impl MovingAverage for SimpleMovingAverage {
94 fn value(&self) -> f64 {
95 self.value
96 }
97
98 fn count(&self) -> usize {
99 self.count
100 }
101
102 fn update_raw(&mut self, value: f64) {
103 self.process_raw(value);
104 }
105}
106
107impl SimpleMovingAverage {
108 #[must_use]
114 pub fn new(period: usize, price_type: Option<PriceType>) -> Self {
115 Self::new_checked(period, price_type).expect(FAILED)
116 }
117
118 pub fn new_checked(period: usize, price_type: Option<PriceType>) -> anyhow::Result<Self> {
124 check_predicate_true(period > 0, "SimpleMovingAverage: period must be > 0")?;
125 check_predicate_true(
126 period <= MAX_PERIOD,
127 &format!("SimpleMovingAverage: period {period} exceeds MAX_PERIOD ({MAX_PERIOD})"),
128 )?;
129
130 Ok(Self {
131 period,
132 price_type: price_type.unwrap_or(PriceType::Last),
133 value: 0.0,
134 sum: 0.0,
135 updates_since_reseed: 0,
136 count: 0,
137 buf: VecDeque::with_capacity(period),
138 initialized: false,
139 })
140 }
141
142 fn process_raw(&mut self, price: f64) {
143 if !price.is_finite() {
144 return;
145 }
146
147 if self.count == self.period {
148 if let Some(oldest) = self.buf.pop_front() {
149 self.sum -= oldest;
150 }
151 } else {
152 self.count += 1;
153 }
154
155 self.buf.push_back(price);
156 self.sum += price;
157 self.updates_since_reseed += 1;
158 if self.updates_since_reseed >= SMA_RESEED_WINDOWS * self.period {
159 self.sum = self.buf.iter().sum();
160 self.updates_since_reseed = 0;
161 }
162
163 self.initialized = self.count >= self.period;
164 if self.initialized {
165 self.value = self.sum / self.period as f64;
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::collections::VecDeque;
173
174 use nautilus_model::{
175 data::{QuoteTick, TradeTick},
176 enums::PriceType,
177 };
178 use proptest::prelude::*;
179 use rstest::rstest;
180
181 use super::MAX_PERIOD;
182 use crate::{
183 average::{sma::SimpleMovingAverage, wma::WeightedMovingAverage},
184 indicator::{Indicator, MovingAverage},
185 stubs::*,
186 };
187
188 #[rstest]
189 #[case(0)]
190 #[case(MAX_PERIOD + 1)]
191 #[case(usize::MAX)]
192 fn test_checked_constructor_rejects_invalid_period(#[case] period: usize) {
193 assert!(SimpleMovingAverage::new_checked(period, None).is_err());
194 }
195
196 #[rstest]
197 fn sma_initialized_state(indicator_sma_10: SimpleMovingAverage) {
198 let display_str = format!("{indicator_sma_10}");
199 assert_eq!(display_str, "SimpleMovingAverage(10)");
200 assert_eq!(indicator_sma_10.period, 10);
201 assert_eq!(indicator_sma_10.price_type, PriceType::Mid);
202 assert_eq!(indicator_sma_10.value, 0.0);
203 assert_eq!(indicator_sma_10.count, 0);
204 assert!(!indicator_sma_10.initialized());
205 assert!(!indicator_sma_10.has_inputs());
206 }
207
208 #[rstest]
209 fn sma_update_raw_exact_period(indicator_sma_10: SimpleMovingAverage) {
210 let mut sma = indicator_sma_10;
211 for i in 1..=10 {
212 sma.update_raw(f64::from(i));
213 }
214 assert!(sma.has_inputs());
215 assert!(sma.initialized());
216 assert_eq!(sma.count, 10);
217 assert_eq!(sma.value, 5.5);
218 }
219
220 #[rstest]
221 fn sma_reset_smoke(indicator_sma_10: SimpleMovingAverage) {
222 let mut sma = indicator_sma_10;
223 sma.update_raw(1.0);
224 assert_eq!(sma.count, 1);
225 sma.reset();
226 assert_eq!(sma.count, 0);
227 assert_eq!(sma.value, 0.0);
228 assert!(!sma.initialized());
229 }
230
231 #[rstest]
232 fn sma_handle_single_quote(indicator_sma_10: SimpleMovingAverage, stub_quote: QuoteTick) {
233 let mut sma = indicator_sma_10;
234 sma.handle_quote(&stub_quote).unwrap();
235 assert_eq!(sma.count, 1);
236 assert_eq!(sma.value, 0.0);
237 assert!(!sma.initialized());
238 }
239
240 #[rstest]
241 fn sma_handle_multiple_quotes(indicator_sma_10: SimpleMovingAverage) {
242 let mut sma = indicator_sma_10;
243 let q1 = stub_quote("1500.0", "1502.0");
244 let q2 = stub_quote("1502.0", "1504.0");
245
246 sma.handle_quote(&q1).unwrap();
247 sma.handle_quote(&q2).unwrap();
248 assert_eq!(sma.count, 2);
249 assert_eq!(sma.value, 0.0);
250 assert!(!sma.initialized());
251 }
252
253 #[rstest]
254 fn sma_handle_trade(indicator_sma_10: SimpleMovingAverage, stub_trade: TradeTick) {
255 let mut sma = indicator_sma_10;
256 sma.handle_trade(&stub_trade);
257 assert_eq!(sma.count, 1);
258 assert_eq!(sma.value, 0.0);
259 assert!(!sma.initialized());
260 }
261
262 #[rstest]
263 #[case(1)]
264 #[case(3)]
265 #[case(5)]
266 #[case(16)]
267 fn count_progression_respects_period(#[case] period: usize) {
268 let mut sma = SimpleMovingAverage::new(period, None);
269
270 for i in 0..(period * 3) {
271 sma.update_raw(i as f64);
272
273 assert!(
274 sma.count() <= period,
275 "period={period}, step={i}, count={}",
276 sma.count()
277 );
278
279 let expected = usize::min(i + 1, period);
280 assert_eq!(
281 sma.count(),
282 expected,
283 "period={period}, step={i}, expected={expected}, was={}",
284 sma.count()
285 );
286 }
287 }
288
289 #[rstest]
290 #[case(1)]
291 #[case(4)]
292 #[case(10)]
293 fn count_after_reset_is_zero(#[case] period: usize) {
294 let mut sma = SimpleMovingAverage::new(period, None);
295
296 for i in 0..(period + 2) {
297 sma.update_raw(i as f64);
298 }
299 assert_eq!(sma.count(), period, "pre-reset saturation failed");
300
301 sma.reset();
302 assert_eq!(sma.count(), 0, "count not reset to zero");
303 assert_eq!(sma.value(), 0.0, "value not reset to zero");
304 assert!(!sma.initialized(), "initialized flag not cleared");
305 }
306
307 #[rstest]
308 fn count_edge_case_period_one() {
309 let mut sma = SimpleMovingAverage::new(1, None);
310
311 sma.update_raw(10.0);
312 assert_eq!(sma.count(), 1);
313 assert_eq!(sma.value(), 10.0);
314
315 sma.update_raw(20.0);
316 assert_eq!(sma.count(), 1, "count exceeded 1 with period==1");
317 assert_eq!(sma.value(), 20.0, "value not equal to latest price");
318 }
319
320 #[rstest]
321 fn sliding_window_correctness() {
322 let mut sma = SimpleMovingAverage::new(3, None);
323
324 let prices = [1.0, 2.0, 3.0, 4.0, 5.0];
325 let expect_avg = [0.0, 0.0, 2.0, 3.0, 4.0];
326
327 for (i, &p) in prices.iter().enumerate() {
328 sma.update_raw(p);
329 assert!(
330 (sma.value() - expect_avg[i]).abs() < 1e-9,
331 "step {i}: expected {}, was {}",
332 expect_avg[i],
333 sma.value()
334 );
335 }
336 }
337
338 proptest! {
339 #[rstest]
340 fn prop_sma_matches_windowed_reference(
341 period in 1usize..=32,
342 inputs in prop::collection::vec(0i64..=1_000_000i64, 1..=96),
343 ) {
344 let mut sma = SimpleMovingAverage::new(period, None);
345 let mut equal_weight_wma = WeightedMovingAverage::with_weights(period, vec![1.0; period], None);
346
347 for (index, input) in inputs.iter().enumerate() {
348 let value = *input as f64;
349 let window_start = index.saturating_add(1).saturating_sub(period);
350 let window = &inputs[window_start..=index];
351 let expected = if window.len() == period { window.iter().sum::<i64>() as f64 / period as f64 } else { 0.0 };
352
353 sma.update_raw(value);
354 equal_weight_wma.update_raw(value);
355
356 prop_assert_eq!(sma.value(), expected);
357 prop_assert_eq!(equal_weight_wma.value(), expected);
358 prop_assert_eq!(sma.count(), window.len());
359 prop_assert_eq!(equal_weight_wma.count(), window.len());
360 prop_assert_eq!(sma.initialized(), index + 1 >= period);
361 prop_assert_eq!(equal_weight_wma.initialized(), index + 1 >= period);
362 }
363 }
364 }
365
366 #[rstest]
367 #[case(2)]
368 #[case(6)]
369 fn initialized_transitions_with_count(#[case] period: usize) {
370 let mut sma = SimpleMovingAverage::new(period, None);
371
372 for i in 0..(period - 1) {
373 sma.update_raw(i as f64);
374 assert!(
375 !sma.initialized(),
376 "initialized early at i={i} (period={period})"
377 );
378 }
379
380 sma.update_raw(42.0);
381 assert_eq!(sma.count(), period);
382 assert!(sma.initialized(), "initialized flag not set at period");
383 }
384
385 #[rstest]
386 #[should_panic(expected = "period must be > 0")]
387 fn sma_new_with_zero_period_panics() {
388 let _ = SimpleMovingAverage::new(0, None);
389 }
390
391 #[rstest]
392 fn sma_rolling_mean_exact_values() {
393 let mut sma = SimpleMovingAverage::new(3, None);
394 let inputs = [1.0, 2.0, 3.0, 4.0, 5.0];
395 let expected = [0.0, 0.0, 2.0, 3.0, 4.0];
396
397 for (&price, &exp_mean) in inputs.iter().zip(expected.iter()) {
398 sma.update_raw(price);
399 assert!(
400 (sma.value() - exp_mean).abs() < 1e-12,
401 "input={price}, expected={exp_mean}, was={}",
402 sma.value()
403 );
404 }
405 }
406
407 #[rstest]
408 fn sma_reseeds_after_a_large_value_leaves_the_window() {
409 let mut sma = SimpleMovingAverage::new(2, None);
410 sma.update_raw(1.0e16);
411 for _ in 0..31 {
412 sma.update_raw(1.0);
413 }
414 assert_eq!(sma.value, 1.0);
415 assert_eq!(sma.count, 2);
416 assert!(sma.initialized);
417 }
418
419 #[rstest]
420 fn sma_matches_reference_implementation() {
421 const PERIOD: usize = 5;
422 let mut sma = SimpleMovingAverage::new(PERIOD, None);
423 let mut window = VecDeque::new();
424
425 for step in 0..20 {
426 let price = f64::from(step) * 10.0;
427 sma.update_raw(price);
428
429 if window.len() == PERIOD {
430 window.pop_front();
431 }
432 window.push_back(price);
433
434 let ref_mean = if window.len() == PERIOD {
435 window.iter().sum::<f64>() / PERIOD as f64
436 } else {
437 0.0
438 };
439 assert!(
440 (sma.value() - ref_mean).abs() < 1e-12,
441 "step={step}, expected={ref_mean}, was={}",
442 sma.value()
443 );
444 }
445 }
446
447 #[rstest]
448 #[case(f64::NAN)]
449 #[case(f64::INFINITY)]
450 #[case(f64::NEG_INFINITY)]
451 fn sma_ignores_bad_floats(#[case] bad: f64) {
452 let mut sma = SimpleMovingAverage::new(3, None);
453 sma.update_raw(1.0);
454 sma.update_raw(bad);
455 sma.update_raw(3.0);
456 assert_eq!(sma.count(), 2);
457 assert_eq!(sma.value(), 0.0);
458 assert!(!sma.initialized());
459 sma.update_raw(5.0);
460 assert_eq!(sma.value(), 3.0);
461 assert!(sma.initialized());
462 }
463
464 #[rstest]
465 fn deque_and_count_always_match() {
466 const PERIOD: usize = 8;
467 let mut sma = SimpleMovingAverage::new(PERIOD, None);
468 for i in 0..50 {
469 sma.update_raw(f64::from(i));
470 assert!(
471 sma.buf.len() == sma.count,
472 "buf.len() != count at step {i}: {} != {}",
473 sma.buf.len(),
474 sma.count
475 );
476 }
477 }
478
479 #[rstest]
480 fn sma_multiple_resets() {
481 let mut sma = SimpleMovingAverage::new(4, None);
482
483 for cycle in 0..5 {
484 for x in 0..4 {
485 sma.update_raw(f64::from(x));
486 }
487 assert!(sma.initialized(), "cycle {cycle}: not initialized");
488 sma.reset();
489 assert_eq!(sma.count(), 0);
490 assert_eq!(sma.value(), 0.0);
491 assert!(!sma.initialized());
492 }
493 }
494
495 #[rstest]
496 fn sma_buffer_never_exceeds_capacity() {
497 const PERIOD: usize = 2_048;
498 let mut sma = super::SimpleMovingAverage::new(PERIOD, None);
499
500 for i in 0..(PERIOD * 2) {
501 sma.update_raw(i as f64);
502
503 assert!(
504 sma.buf.len() <= PERIOD,
505 "step {i}: buf.len()={}, exceeds PERIOD={PERIOD}",
506 sma.buf.len(),
507 );
508 }
509 assert!(
510 sma.buf.len() == PERIOD,
511 "buffer not reported as full after saturation"
512 );
513 assert_eq!(
514 sma.count(),
515 PERIOD,
516 "count diverged from logical window length"
517 );
518 }
519
520 #[rstest]
521 fn sma_deque_eviction_order() {
522 let mut sma = super::SimpleMovingAverage::new(3, None);
523
524 sma.update_raw(1.0);
525 sma.update_raw(2.0);
526 sma.update_raw(3.0);
527 sma.update_raw(4.0);
528
529 assert_eq!(sma.buf.front().copied(), Some(2.0), "oldest element wrong");
530 assert_eq!(sma.buf.back().copied(), Some(4.0), "newest element wrong");
531
532 assert!(
533 (sma.value() - 3.0).abs() < 1e-12,
534 "unexpected mean after eviction: {}",
535 sma.value()
536 );
537 }
538
539 #[rstest]
540 fn sma_sum_consistent_with_buffer() {
541 const PERIOD: usize = 7;
542 let mut sma = super::SimpleMovingAverage::new(PERIOD, None);
543
544 for i in 0..40 {
545 sma.update_raw(f64::from(i));
546
547 let deque_sum: f64 = sma.buf.iter().copied().sum();
548 assert!(
549 (sma.sum - deque_sum).abs() < 1e-12,
550 "step {i}: internal sum={} differs from buf sum={}",
551 sma.sum,
552 deque_sum
553 );
554 }
555 }
556
557 #[rstest]
558 fn test_periodic_reseed_preserves_chronological_additions() {
559 let mut sma = SimpleMovingAverage::new(3, None);
560
561 for _ in 0..45 {
562 sma.update_raw(0.0);
563 }
564 sma.update_raw(1e308);
565 sma.update_raw(-1e308);
566 sma.update_raw(1e-100);
567
568 assert_eq!(sma.value(), 1e-100 / 3.0);
569 }
570}