Skip to main content

oxihuman_core/
moving_avg_calc.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Exponential/simple moving average calculator (enhanced).
6
7/// Simple Moving Average over a sliding window.
8#[derive(Debug, Clone)]
9pub struct SimpleMaCalc {
10    window: Vec<f64>,
11    size: usize,
12    pos: usize,
13    filled: bool,
14    sum: f64,
15}
16
17impl SimpleMaCalc {
18    pub fn new(size: usize) -> Self {
19        assert!(size > 0, "window size must be positive");
20        SimpleMaCalc {
21            window: vec![0.0; size],
22            size,
23            pos: 0,
24            filled: false,
25            sum: 0.0,
26        }
27    }
28
29    pub fn update(&mut self, value: f64) -> f64 {
30        self.sum -= self.window[self.pos];
31        self.window[self.pos] = value;
32        self.sum += value;
33        self.pos += 1;
34        if self.pos >= self.size {
35            self.pos = 0;
36            self.filled = true;
37        }
38        self.current()
39    }
40
41    pub fn current(&self) -> f64 {
42        let count = if self.filled {
43            self.size
44        } else {
45            self.pos.max(1)
46        };
47        self.sum / count as f64
48    }
49
50    pub fn is_ready(&self) -> bool {
51        self.filled
52    }
53
54    pub fn window_size(&self) -> usize {
55        self.size
56    }
57}
58
59/// Exponential Moving Average.
60#[derive(Debug, Clone)]
61pub struct EmaCalc {
62    alpha: f64,
63    value: Option<f64>,
64}
65
66impl EmaCalc {
67    /// `alpha` is the smoothing factor in [0, 1]. Smaller = more smoothing.
68    pub fn new(alpha: f64) -> Self {
69        let alpha = alpha.clamp(0.0, 1.0);
70        EmaCalc { alpha, value: None }
71    }
72
73    /// Create EMA from a period: alpha = 2 / (period + 1).
74    pub fn from_period(period: usize) -> Self {
75        let alpha = 2.0 / (period as f64 + 1.0);
76        EmaCalc::new(alpha)
77    }
78
79    pub fn update(&mut self, value: f64) -> f64 {
80        let ema = match self.value {
81            None => value,
82            Some(prev) => self.alpha * value + (1.0 - self.alpha) * prev,
83        };
84        self.value = Some(ema);
85        ema
86    }
87
88    pub fn current(&self) -> Option<f64> {
89        self.value
90    }
91
92    pub fn alpha(&self) -> f64 {
93        self.alpha
94    }
95}
96
97pub fn sma_batch(data: &[f64], window: usize) -> Vec<f64> {
98    let mut ma = SimpleMaCalc::new(window);
99    data.iter().map(|&v| ma.update(v)).collect()
100}
101
102pub fn ema_batch(data: &[f64], period: usize) -> Vec<f64> {
103    let mut ema = EmaCalc::from_period(period);
104    data.iter().map(|&v| ema.update(v)).collect()
105}
106
107pub fn ma_crossover(fast: &[f64], slow: &[f64]) -> Vec<f64> {
108    fast.iter().zip(slow.iter()).map(|(f, s)| f - s).collect()
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_sma_single() {
117        let mut ma = SimpleMaCalc::new(3);
118        let v = ma.update(10.0);
119        assert!((v - 10.0).abs() < 1e-10, /* single value SMA = itself */);
120    }
121
122    #[test]
123    fn test_sma_window() {
124        let mut ma = SimpleMaCalc::new(3);
125        ma.update(1.0);
126        ma.update(2.0);
127        let v = ma.update(3.0);
128        assert!((v - 2.0).abs() < 1e-10 /* (1+2+3)/3 = 2 */,);
129    }
130
131    #[test]
132    fn test_sma_ready() {
133        let mut ma = SimpleMaCalc::new(3);
134        ma.update(1.0);
135        ma.update(2.0);
136        assert!(!ma.is_ready() /* not filled yet */,);
137        ma.update(3.0);
138        assert!(ma.is_ready() /* now filled */,);
139    }
140
141    #[test]
142    fn test_ema_first_value() {
143        let mut ema = EmaCalc::new(0.5);
144        let v = ema.update(100.0);
145        assert!((v - 100.0).abs() < 1e-10 /* first EMA = first value */,);
146    }
147
148    #[test]
149    fn test_ema_smoothing() {
150        let mut ema = EmaCalc::new(0.5);
151        ema.update(100.0);
152        let v = ema.update(0.0);
153        assert!((v - 50.0).abs() < 1e-10 /* 0.5*0 + 0.5*100 = 50 */,);
154    }
155
156    #[test]
157    fn test_ema_from_period() {
158        let ema = EmaCalc::from_period(9);
159        assert!((ema.alpha() - 0.2).abs() < 1e-10, /* alpha = 2/10 = 0.2 */);
160    }
161
162    #[test]
163    fn test_sma_batch_length() {
164        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
165        let result = sma_batch(&data, 3);
166        assert_eq!(result.len(), 5);
167    }
168
169    #[test]
170    fn test_ema_batch_length() {
171        let data = vec![1.0, 2.0, 3.0, 4.0];
172        let result = ema_batch(&data, 3);
173        assert_eq!(result.len(), 4);
174    }
175
176    #[test]
177    fn test_crossover_length() {
178        let fast = vec![1.0, 2.0, 3.0];
179        let slow = vec![1.5, 1.5, 1.5];
180        let cross = ma_crossover(&fast, &slow);
181        assert_eq!(cross.len(), 3);
182        assert!((cross[2] - 1.5).abs() < 1e-10 /* 3 - 1.5 = 1.5 */,);
183    }
184}