Skip to main content

oxihuman_core/
moving_average.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Simple, exponential, and weighted moving averages.
6
7use std::collections::VecDeque;
8
9/// Simple Moving Average (SMA) over a sliding window.
10pub struct SimpleMovingAverage {
11    window: VecDeque<f64>,
12    size: usize,
13    sum: f64,
14}
15
16/// Construct a new SimpleMovingAverage.
17pub fn new_sma(window_size: usize) -> SimpleMovingAverage {
18    SimpleMovingAverage {
19        window: VecDeque::new(),
20        size: window_size.max(1),
21        sum: 0.0,
22    }
23}
24
25impl SimpleMovingAverage {
26    /// Push a value and return the current SMA.
27    pub fn push(&mut self, x: f64) -> f64 {
28        self.window.push_back(x);
29        self.sum += x;
30        if self.window.len() > self.size {
31            self.sum -= self.window.pop_front().unwrap_or(0.0);
32        }
33        self.current()
34    }
35
36    /// Current SMA value.
37    pub fn current(&self) -> f64 {
38        if self.window.is_empty() {
39            0.0
40        } else {
41            self.sum / self.window.len() as f64
42        }
43    }
44
45    /// Number of values in the window.
46    pub fn len(&self) -> usize {
47        self.window.len()
48    }
49
50    /// Whether the window is empty.
51    pub fn is_empty(&self) -> bool {
52        self.window.is_empty()
53    }
54
55    /// Whether the window is full.
56    pub fn is_full(&self) -> bool {
57        self.window.len() == self.size
58    }
59
60    /// Reset the averager.
61    pub fn reset(&mut self) {
62        self.window.clear();
63        self.sum = 0.0;
64    }
65}
66
67/// Exponential Moving Average (EMA).
68pub struct ExponentialMovingAverage {
69    alpha: f64,
70    value: Option<f64>,
71}
72
73/// Construct a new EMA with smoothing factor `alpha` in (0, 1].
74pub fn new_ema(alpha: f64) -> ExponentialMovingAverage {
75    let alpha = alpha.clamp(1e-6, 1.0);
76    ExponentialMovingAverage { alpha, value: None }
77}
78
79impl ExponentialMovingAverage {
80    /// Push a value and return the current EMA.
81    pub fn push(&mut self, x: f64) -> f64 {
82        self.value = Some(match self.value {
83            None => x,
84            Some(prev) => self.alpha * x + (1.0 - self.alpha) * prev,
85        });
86        self.value.unwrap_or_default()
87    }
88
89    /// Current EMA (None if no values pushed).
90    pub fn current(&self) -> Option<f64> {
91        self.value
92    }
93
94    /// Smoothing factor.
95    pub fn alpha(&self) -> f64 {
96        self.alpha
97    }
98
99    /// Reset.
100    pub fn reset(&mut self) {
101        self.value = None;
102    }
103}
104
105/// Weighted Moving Average (WMA).
106pub struct WeightedMovingAverage {
107    window: VecDeque<f64>,
108    size: usize,
109}
110
111/// Construct a new WMA.
112pub fn new_wma(window_size: usize) -> WeightedMovingAverage {
113    WeightedMovingAverage {
114        window: VecDeque::new(),
115        size: window_size.max(1),
116    }
117}
118
119impl WeightedMovingAverage {
120    /// Push a value and return the current WMA.
121    pub fn push(&mut self, x: f64) -> f64 {
122        self.window.push_back(x);
123        if self.window.len() > self.size {
124            self.window.pop_front();
125        }
126        self.current()
127    }
128
129    /// Compute the WMA (more recent values have higher weight).
130    pub fn current(&self) -> f64 {
131        let n = self.window.len();
132        if n == 0 {
133            return 0.0;
134        }
135        let denom = (n * (n + 1)) as f64 / 2.0;
136        self.window
137            .iter()
138            .enumerate()
139            .map(|(i, &v)| v * (i + 1) as f64)
140            .sum::<f64>()
141            / denom
142    }
143
144    /// Reset.
145    pub fn reset(&mut self) {
146        self.window.clear();
147    }
148}
149
150/// Apply SMA to a slice, returning a vector of the same length.
151pub fn apply_sma(data: &[f64], window_size: usize) -> Vec<f64> {
152    let mut sma = new_sma(window_size);
153    data.iter().map(|&x| sma.push(x)).collect()
154}
155
156/// Apply EMA to a slice.
157pub fn apply_ema(data: &[f64], alpha: f64) -> Vec<f64> {
158    let mut ema = new_ema(alpha);
159    data.iter().map(|&x| ema.push(x)).collect()
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_sma_constant() {
168        /* SMA of constant values equals the constant */
169        let mut sma = new_sma(3);
170        sma.push(5.0);
171        sma.push(5.0);
172        let v = sma.push(5.0);
173        assert!((v - 5.0).abs() < 1e-12);
174    }
175
176    #[test]
177    fn test_sma_window_full() {
178        /* is_full returns true after window_size values */
179        let mut sma = new_sma(3);
180        sma.push(1.0);
181        sma.push(2.0);
182        assert!(!sma.is_full());
183        sma.push(3.0);
184        assert!(sma.is_full());
185    }
186
187    #[test]
188    fn test_sma_sliding() {
189        /* SMA slides: pushing beyond window size drops oldest */
190        let mut sma = new_sma(2);
191        sma.push(10.0);
192        sma.push(20.0);
193        let v = sma.push(30.0);
194        assert!((v - 25.0).abs() < 1e-12, "v={v}");
195    }
196
197    #[test]
198    fn test_ema_first_value() {
199        /* EMA first value equals the pushed value */
200        let mut ema = new_ema(0.5);
201        let v = ema.push(10.0);
202        assert!((v - 10.0).abs() < 1e-12);
203    }
204
205    #[test]
206    fn test_ema_smoothing() {
207        /* EMA damps large spikes */
208        let mut ema = new_ema(0.1);
209        for _ in 0..100 {
210            ema.push(0.0);
211        }
212        let v = ema.push(100.0);
213        assert!(v < 20.0, "v={v}");
214    }
215
216    #[test]
217    fn test_wma_current() {
218        /* WMA weights recent values more heavily */
219        let mut wma = new_wma(3);
220        wma.push(1.0);
221        wma.push(2.0);
222        let v = wma.push(3.0);
223        /* weights: 1,2,3 => (1*1 + 2*2 + 3*3)/(1+2+3) = 14/6 ~ 2.33 */
224        assert!((v - 14.0 / 6.0).abs() < 1e-10, "v={v}");
225    }
226
227    #[test]
228    fn test_apply_sma_length() {
229        /* apply_sma returns same length as input */
230        let data: Vec<f64> = (0..10).map(|i| i as f64).collect();
231        let out = apply_sma(&data, 3);
232        assert_eq!(out.len(), 10);
233    }
234
235    #[test]
236    fn test_apply_ema_length() {
237        /* apply_ema returns same length as input */
238        let data = vec![1.0f64; 5];
239        assert_eq!(apply_ema(&data, 0.3).len(), 5);
240    }
241
242    #[test]
243    fn test_sma_reset() {
244        /* reset clears sma state */
245        let mut sma = new_sma(3);
246        sma.push(1.0);
247        sma.reset();
248        assert_eq!(sma.len(), 0);
249    }
250}