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