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}