Skip to main content

quant_indicators/
wma.rs

1//! Weighted Moving Average (WMA) 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/// Weighted Moving Average indicator.
11///
12/// Assigns linearly increasing weights to more recent prices.
13/// Most recent price has weight = period, oldest has weight = 1.
14///
15/// # Formula
16///
17/// WMA = (P1*1 + P2*2 + ... + Pn*n) / (1 + 2 + ... + n)
18///
19/// where n = period and Pn is the most recent price.
20///
21/// # Example
22///
23/// ```
24/// use quant_indicators::{Indicator, Wma};
25/// use quant_primitives::Candle;
26/// use chrono::Utc;
27/// use rust_decimal_macros::dec;
28///
29/// let ts = Utc::now();
30/// let candles: Vec<Candle> = (0..20).map(|i| {
31///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
32/// }).collect();
33/// let wma = Wma::new(20).unwrap();
34/// let series = wma.compute(&candles).unwrap();
35/// ```
36#[derive(Debug, Clone)]
37pub struct Wma {
38    period: usize,
39    weight_sum: Decimal,
40    name: String,
41}
42
43impl Wma {
44    /// Create a new WMA indicator with the specified period.
45    ///
46    /// # Errors
47    ///
48    /// Returns `InvalidParameter` if period is 0.
49    pub fn new(period: usize) -> Result<Self, IndicatorError> {
50        if period == 0 {
51            return Err(IndicatorError::InvalidParameter {
52                message: "WMA period must be > 0".to_string(),
53            });
54        }
55        // Sum of weights: 1 + 2 + ... + n = n(n+1)/2
56        let weight_sum =
57            Decimal::from(period as u64) * Decimal::from(period as u64 + 1) / Decimal::TWO;
58        Ok(Self {
59            period,
60            weight_sum,
61            name: format!("WMA({})", period),
62        })
63    }
64}
65
66impl Indicator for Wma {
67    fn name(&self) -> &str {
68        &self.name
69    }
70
71    fn warmup_period(&self) -> usize {
72        self.period
73    }
74
75    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
76        if candles.len() < self.period {
77            return Err(IndicatorError::InsufficientData {
78                required: self.period,
79                actual: candles.len(),
80            });
81        }
82
83        let mut values = Vec::with_capacity(candles.len() - self.period + 1);
84
85        for window in candles.windows(self.period) {
86            let weighted_sum: Decimal = window
87                .iter()
88                .enumerate()
89                .map(|(i, c)| c.close() * Decimal::from((i + 1) as u64))
90                .sum();
91
92            let wma = weighted_sum / self.weight_sum;
93            let ts = window[self.period - 1].timestamp();
94            values.push((ts, wma));
95        }
96
97        Ok(Series::new(values))
98    }
99}
100
101#[cfg(test)]
102#[path = "wma_tests.rs"]
103mod tests;