Skip to main content

hopr_utils/statistics/moving/
exponential.rs

1/// An exponential moving average calculator.
2///
3/// Based on the formula:
4///
5/// EMA_t = EMA_{t-1} + (Value_t - EMA_{t-1}) / min(t, FACTOR)
6///
7/// this object maintains a running average that emphasizes recent values more heavily
8/// and creates a smooth long-tailed averaging effect over time.
9#[derive(Debug, Copy, Clone, Default, PartialEq)]
10pub struct ExponentialMovingAverage<const FACTOR: usize> {
11    count: usize,
12    average: f64,
13}
14
15impl<const FACTOR: usize> ExponentialMovingAverage<FACTOR> {
16    const _ASSERT_FACTOR_GT_ZERO: () = assert!(FACTOR > 0, "FACTOR must be greater than 0");
17
18    /// Updates the moving average with a new value.
19    pub fn update(&mut self, value: impl Into<f64>) {
20        let value: f64 = value.into();
21        self.count += 1;
22        self.average = self.average + (value - self.average) / (std::cmp::min(self.count, FACTOR) as f64);
23    }
24
25    /// Retrieves the current value of the moving average.
26    pub fn get(&self) -> f64 {
27        self.average
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use assertables::{assert_f64_eq, assert_in_delta};
34
35    #[test]
36    fn running_average_should_compute_the_windowed_average_correctly() {
37        let mut avg = super::ExponentialMovingAverage::<5>::default();
38
39        for i in 1..=10 {
40            avg.update(i);
41        }
42
43        assert_in_delta!(avg.get(), 6.6, 0.1);
44    }
45
46    #[test]
47    fn running_average_should_compute_the_average_from_constant_correctly() {
48        let mut avg = super::ExponentialMovingAverage::<5>::default();
49
50        for _ in 1..=10 {
51            avg.update(3);
52        }
53
54        assert_f64_eq!(avg.get(), 3.0);
55    }
56}