indexes_rs/v1/sma/
main.rs

1//! A Simple Moving Average (SMA) calculator module.
2//!
3//! The Simple Moving Average is calculated by taking the arithmetic mean of a given set of values
4//! over a specified period. For example, a 20-day SMA is calculated by taking the arithmetic mean
5//! of the most recent 20 values.
6//!
7//! This implementation uses a sliding window backed by a [`VecDeque`](https://doc.rust-lang.org/std/collections/struct.VecDeque.html)
8//! and maintains a running sum for improved performance. In addition to calculating the SMA value,
9//! it also determines the trend (Up, Down, or Neutral) based on the change from the previous SMA.
10//!
11//! # Examples
12//!
13//! Using the SMA with a period of 3:
14//!
15//! ```rust
16//! use indexes_rs::v1::sma::main::{SimpleMovingAverage, SMAError, SMAResult};
17//! use indexes_rs::v1::types::TrendDirection;
18//!
19//! let mut sma = SimpleMovingAverage::new(3).unwrap();
20//! sma.add_value(2.0);
21//! sma.add_value(4.0);
22//! sma.add_value(6.0);
23//!
24//! // First calculation returns Sideways trend since no previous value exists.
25//! let result = sma.calculate().unwrap();
26//! assert_eq!(result.value, 4.0);
27//! assert_eq!(result.trend, TrendDirection::Sideways);
28//! ```
29
30pub use super::types::{SMAError, SMAResult};
31use crate::v1::types::TrendDirection;
32use std::collections::VecDeque;
33
34/// A Simple Moving Average (SMA) calculator that maintains a moving window of values
35/// and calculates their average along with a trend indicator.
36///
37/// The SMA is calculated by taking the arithmetic mean of a given set of values
38/// over a specified period. The trend is determined by comparing the current SMA value
39/// with the previous SMA value:
40/// - If the current SMA is greater, the trend is `Up`.
41/// - If it is lower, the trend is `Down`.
42/// - If it is the same (or if no previous value exists), the trend is `Sideways`.
43#[derive(Debug, PartialEq)]
44pub struct SimpleMovingAverage {
45    /// The period over which the moving average is calculated.
46    pub period: usize,
47    /// The collection of values in the moving window.
48    values: VecDeque<f64>,
49    /// The running sum of the values in the window.
50    sum: f64,
51    /// The previous calculated SMA value.
52    last_value: Option<f64>,
53}
54
55impl SimpleMovingAverage {
56    /// Creates a new `SimpleMovingAverage` instance with the specified period.
57    ///
58    /// # Arguments
59    ///
60    /// * `period` - The number of values to include in the moving average calculation.
61    ///
62    /// # Returns
63    ///
64    /// * `Ok(SimpleMovingAverage)` - A new instance with the specified period.
65    /// * `Err(SMAError)` - If the period is invalid (e.g., zero).
66    ///
67    /// # Examples
68    ///
69    /// Using a valid period:
70    ///
71    /// ```rust
72    /// use indexes_rs::v1::sma::main::{SimpleMovingAverage, SMAError};
73    ///
74    /// let sma = SimpleMovingAverage::new(3);
75    /// assert!(sma.is_ok());
76    /// ```
77    ///
78    /// Using an invalid period:
79    ///
80    /// ```rust
81    /// use indexes_rs::v1::sma::main::{SimpleMovingAverage, SMAError};
82    ///
83    /// let sma = SimpleMovingAverage::new(0);
84    /// assert_eq!(sma, Err(SMAError::InvalidPeriod));
85    /// ```
86    pub fn new(period: usize) -> Result<Self, SMAError> {
87        if period == 0 {
88            return Err(SMAError::InvalidPeriod);
89        }
90        Ok(SimpleMovingAverage {
91            period,
92            values: VecDeque::with_capacity(period),
93            sum: 0.0,
94            last_value: None,
95        })
96    }
97
98    /// Adds a new value to the moving window.
99    ///
100    /// If the window is full (i.e., the number of stored values equals the period),
101    /// the oldest value is removed before adding the new one.
102    ///
103    /// # Arguments
104    ///
105    /// * `value` - The new value to add to the moving window.
106    ///
107    /// # Examples
108    ///
109    /// ```rust
110    /// use indexes_rs::v1::sma::main::SimpleMovingAverage;
111    ///
112    /// let mut sma = SimpleMovingAverage::new(3).unwrap();
113    /// sma.add_value(2.0);
114    /// sma.add_value(4.0);
115    /// sma.add_value(6.0);
116    /// ```
117    pub fn add_value(&mut self, value: f64) {
118        if self.values.len() == self.period {
119            if let Some(old_value) = self.values.pop_front() {
120                self.sum -= old_value;
121            }
122        }
123        self.values.push_back(value);
124        self.sum += value;
125    }
126
127    /// Calculates the current Simple Moving Average and determines the trend.
128    ///
129    /// # Returns
130    ///
131    /// * `Some(SMAResult)` - The calculated SMA and trend if enough values are available.
132    /// * `None` - If there aren't enough values to calculate the average.
133    ///
134    /// # Examples
135    ///
136    /// ```rust
137    /// use indexes_rs::v1::sma::main::{SimpleMovingAverage, SMAError, SMAResult};
138    /// use indexes_rs::v1::types::TrendDirection;
139    ///
140    /// let mut sma = SimpleMovingAverage::new(3).unwrap();
141    /// sma.add_value(2.0);
142    /// sma.add_value(4.0);
143    /// sma.add_value(6.0);
144    ///
145    /// let result = sma.calculate().unwrap();
146    /// // The average of [2.0, 4.0, 6.0] is 4.0, and since no previous SMA exists, the trend is Sideways.
147    /// assert_eq!(result.value, 4.0);
148    /// assert_eq!(result.trend, TrendDirection::Sideways);
149    /// ```
150    pub fn calculate(&mut self) -> Option<SMAResult> {
151        if self.values.len() < self.period {
152            return None;
153        }
154        let current_sma = self.sum / self.period as f64;
155        let trend = match self.last_value {
156            Some(prev) if current_sma > prev => TrendDirection::Up,
157            Some(prev) if current_sma < prev => TrendDirection::Down,
158            _ => TrendDirection::Sideways,
159        };
160        self.last_value = Some(current_sma);
161        Some(SMAResult { value: current_sma, trend })
162    }
163}