Skip to main content

quant_indicators/
sma.rs

1//! Simple Moving Average (SMA) indicator.
2
3use quant_primitives::Candle;
4use rust_decimal::Decimal;
5
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9
10/// Simple Moving Average indicator.
11///
12/// Computes the arithmetic mean of closing prices over the specified period.
13///
14/// # Formula
15///
16/// SMA = (P1 + P2 + ... + Pn) / n
17///
18/// where P is the closing price and n is the period.
19///
20/// # Example
21///
22/// ```
23/// use quant_indicators::{Indicator, Sma};
24/// use quant_primitives::Candle;
25/// use chrono::Utc;
26/// use rust_decimal_macros::dec;
27///
28/// let ts = Utc::now();
29/// let candles: Vec<Candle> = (0..20).map(|i| {
30///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
31/// }).collect();
32/// let sma = Sma::new(20).unwrap();
33/// let series = sma.compute(&candles).unwrap();
34/// ```
35#[derive(Debug, Clone)]
36pub struct Sma {
37    period: usize,
38    name: String,
39}
40
41impl Sma {
42    /// Create a new SMA indicator with the specified period.
43    ///
44    /// # Errors
45    ///
46    /// Returns `InvalidParameter` if period is 0.
47    pub fn new(period: usize) -> Result<Self, IndicatorError> {
48        if period == 0 {
49            return Err(IndicatorError::InvalidParameter {
50                message: "SMA period must be > 0".to_string(),
51            });
52        }
53        Ok(Self {
54            period,
55            name: format!("SMA({})", period),
56        })
57    }
58}
59
60impl Indicator for Sma {
61    fn name(&self) -> &str {
62        &self.name
63    }
64
65    fn warmup_period(&self) -> usize {
66        self.period
67    }
68
69    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
70        if candles.len() < self.period {
71            return Err(IndicatorError::InsufficientData {
72                required: self.period,
73                actual: candles.len(),
74            });
75        }
76
77        let mut values = Vec::with_capacity(candles.len() - self.period + 1);
78        let period_dec = Decimal::from(self.period as u64);
79
80        for window in candles.windows(self.period) {
81            let sum: Decimal = window.iter().map(|c| c.close()).sum();
82            let avg = sum / period_dec;
83            // Safe: windows(n) always yields slices of length n when n > 0
84            let ts = window[self.period - 1].timestamp();
85            values.push((ts, avg));
86        }
87
88        Ok(Series::new(values))
89    }
90}
91
92#[cfg(test)]
93#[path = "sma_tests.rs"]
94mod tests;