mantis_ta/indicators/volume/
volume_sma.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
31pub struct VolumeSMA {
32 period: usize,
33 sum: f64,
34 window: RingBuf<f64>,
35}
36
37impl VolumeSMA {
38 pub fn new(period: usize) -> Self {
39 assert!(period > 0, "period must be > 0");
40 Self {
41 period,
42 sum: 0.0,
43 window: RingBuf::new(period, 0.0),
44 }
45 }
46
47 fn update(&mut self, vol: f64) -> Option<f64> {
48 if let Some(old) = self.window.push(vol) {
49 self.sum -= old;
50 }
51 self.sum += vol;
52 if self.window.len() < self.period {
53 None
54 } else {
55 Some(self.sum / self.period as f64)
56 }
57 }
58}
59
60impl Indicator for VolumeSMA {
61 type Output = f64;
62
63 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
64 self.update(candle.volume)
65 }
66
67 fn reset(&mut self) {
68 self.sum = 0.0;
69 self.window = RingBuf::new(self.period, 0.0);
70 }
71
72 fn warmup_period(&self) -> usize {
73 self.period
74 }
75
76 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
77 Box::new(self.clone())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn volume_sma_emits_after_warmup() {
87 let mut v_sma = VolumeSMA::new(3);
88 let vols = [10.0, 20.0, 30.0, 40.0];
89 let candles: Vec<Candle> = vols
90 .iter()
91 .map(|v| Candle {
92 timestamp: 0,
93 open: 0.0,
94 high: 0.0,
95 low: 0.0,
96 close: 0.0,
97 volume: *v,
98 })
99 .collect();
100
101 let outputs: Vec<_> = candles.iter().map(|c| v_sma.next(c)).collect();
102 assert!(outputs
103 .iter()
104 .take(v_sma.warmup_period() - 1)
105 .all(|o| o.is_none()));
106 assert!(outputs
107 .iter()
108 .skip(v_sma.warmup_period() - 1)
109 .any(|o| o.is_some()));
110 }
111}