hopr_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    /// Updates the moving average with a new value.
17    pub fn update(&mut self, value: impl Into<f64>) {
18        let value: f64 = value.into();
19        self.count += 1;
20        self.average = self.average + (value - self.average) / (std::cmp::min(self.count, FACTOR) as f64);
21    }
22
23    /// Retrieves the current value of the moving average.
24    pub fn get(&self) -> f64 {
25        self.average
26    }
27}
28
29#[cfg(test)]
30mod tests {
31    use assertables::{assert_f64_eq, assert_in_delta};
32
33    #[test]
34    fn running_average_should_compute_the_windowed_average_correctly() {
35        let mut avg = super::ExponentialMovingAverage::<5>::default();
36
37        for i in 1..=10 {
38            avg.update(i);
39        }
40
41        assert_in_delta!(avg.get(), 6.6, 0.1);
42    }
43
44    #[test]
45    fn running_average_should_compute_the_average_from_constant_correctly() {
46        let mut avg = super::ExponentialMovingAverage::<5>::default();
47
48        for _ in 1..=10 {
49            avg.update(3);
50        }
51
52        assert_f64_eq!(avg.get(), 3.0);
53    }
54}