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;