mantis_ta/indicators/trend/
wma.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
35pub struct WMA {
36 period: usize,
37 window: RingBuf<f64>,
38 divisor: f64,
39}
40
41impl WMA {
42 pub fn new(period: usize) -> Self {
43 assert!(period > 0, "period must be > 0");
44 let divisor = (period * (period + 1) / 2) as f64;
45 Self {
46 period,
47 window: RingBuf::new(period, 0.0),
48 divisor,
49 }
50 }
51
52 #[inline]
53 fn update(&mut self, value: f64) -> Option<f64> {
54 self.window.push(value);
55
56 if self.window.len() < self.period {
57 return None;
58 }
59
60 let weighted_sum: f64 = self
61 .window
62 .iter()
63 .enumerate()
64 .map(|(i, &v)| v * ((i + 1) as f64))
65 .sum();
66
67 Some(weighted_sum / self.divisor)
68 }
69}
70
71impl Indicator for WMA {
72 type Output = f64;
73
74 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
75 self.update(candle.close)
76 }
77
78 fn reset(&mut self) {
79 self.window = RingBuf::new(self.period, 0.0);
80 }
81
82 fn warmup_period(&self) -> usize {
83 self.period
84 }
85
86 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
87 Box::new(self.clone())
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn computes_wma_after_warmup() {
97 let mut wma = WMA::new(3);
98 let candles = [1.0, 2.0, 3.0, 4.0]
99 .iter()
100 .map(|c| Candle {
101 timestamp: 0,
102 open: *c,
103 high: *c,
104 low: *c,
105 close: *c,
106 volume: 0.0,
107 })
108 .collect::<Vec<_>>();
109
110 let mut outputs = Vec::new();
111 for c in &candles {
112 outputs.push(wma.next(c));
113 }
114
115 assert_eq!(outputs[0], None);
116 assert_eq!(outputs[1], None);
117 assert!(outputs[2].is_some());
119 let wma_val = outputs[2].unwrap();
120 assert!((wma_val - 2.333333).abs() < 0.0001);
121 assert!(outputs[3].is_some());
122 }
123
124 #[test]
125 fn wma_reset_clears_state() {
126 let mut wma = WMA::new(3);
127 let candle = Candle {
128 timestamp: 0,
129 open: 1.0,
130 high: 1.0,
131 low: 1.0,
132 close: 1.0,
133 volume: 0.0,
134 };
135
136 wma.next(&candle);
137 wma.next(&candle);
138 wma.next(&candle);
139 assert!(wma.next(&candle).is_some());
140
141 wma.reset();
142 assert_eq!(wma.next(&candle), None);
143 }
144
145 #[test]
146 fn wma_with_constant_values() {
147 let mut wma = WMA::new(2);
148 let candles = [5.0, 5.0, 5.0]
149 .iter()
150 .map(|c| Candle {
151 timestamp: 0,
152 open: *c,
153 high: *c,
154 low: *c,
155 close: *c,
156 volume: 0.0,
157 })
158 .collect::<Vec<_>>();
159
160 let outputs: Vec<_> = candles.iter().map(|c| wma.next(c)).collect();
161 assert_eq!(outputs[0], None);
162 assert_eq!(outputs[1], Some(5.0));
163 assert_eq!(outputs[2], Some(5.0));
164 }
165
166 #[test]
167 fn wma_warmup_period() {
168 let wma = WMA::new(5);
169 assert_eq!(wma.warmup_period(), 5);
170 }
171}