wickra_core/indicators/
mom.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
31pub struct Mom {
32 period: usize,
33 window: VecDeque<f64>,
35 last: Option<f64>,
36}
37
38impl Mom {
39 pub fn new(period: usize) -> Result<Self> {
45 if period == 0 {
46 return Err(Error::PeriodZero);
47 }
48 if period > crate::error::MAX_PERIOD {
49 return Err(Error::InvalidPeriod {
50 message: crate::error::PERIOD_ABOVE_MAX,
51 });
52 }
53 Ok(Self {
54 period,
55 window: VecDeque::with_capacity(period + 1),
56 last: None,
57 })
58 }
59
60 pub const fn period(&self) -> usize {
62 self.period
63 }
64
65 pub const fn value(&self) -> Option<f64> {
67 self.last
68 }
69}
70
71impl Indicator for Mom {
72 type Input = f64;
73 type Output = f64;
74
75 #[inline]
76 fn update(&mut self, input: f64) -> Option<f64> {
77 if !input.is_finite() {
78 return None;
80 }
81 if self.window.len() == self.period + 1 {
82 self.window.pop_front();
83 }
84 self.window.push_back(input);
85 if self.window.len() < self.period + 1 {
86 return None;
87 }
88 let prev = *self.window.front().expect("window is non-empty");
89 let mom = input - prev;
90 self.last = Some(mom);
91 Some(mom)
92 }
93
94 fn reset(&mut self) {
95 self.window.clear();
96 self.last = None;
97 }
98
99 #[inline]
100 fn warmup_period(&self) -> usize {
101 self.period + 1
102 }
103
104 #[inline]
105 fn is_ready(&self) -> bool {
106 self.window.len() == self.period + 1
107 }
108
109 #[inline]
110 fn name(&self) -> &'static str {
111 "MOM"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119 use approx::assert_relative_eq;
120
121 #[test]
122 fn new_rejects_zero_period() {
123 assert!(matches!(Mom::new(0), Err(Error::PeriodZero)));
124 }
125
126 #[test]
130 fn accessors_and_metadata() {
131 let mut m = Mom::new(5).unwrap();
132 assert_eq!(m.period(), 5);
133 assert_eq!(m.name(), "MOM");
134 assert_eq!(m.value(), None);
135 for i in 1..=6 {
136 m.update(f64::from(i));
137 }
138 assert!(m.value().is_some());
139 }
140
141 #[test]
142 fn reference_values() {
143 let mut mom = Mom::new(3).unwrap();
145 let out = mom.batch(&[1.0, 2.0, 3.0, 4.0, 7.0]);
146 assert_eq!(mom.warmup_period(), 4);
147 assert_eq!(out[0], None);
148 assert_eq!(out[2], None);
149 assert_relative_eq!(out[3].unwrap(), 4.0 - 1.0, epsilon = 1e-12);
150 assert_relative_eq!(out[4].unwrap(), 7.0 - 2.0, epsilon = 1e-12);
151 }
152
153 #[test]
154 fn constant_series_yields_zero() {
155 let mut mom = Mom::new(5).unwrap();
156 let out = mom.batch(&[10.0; 20]);
157 for v in out.iter().skip(5).flatten() {
158 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
159 }
160 }
161
162 #[test]
163 fn ignores_non_finite_input() {
164 let mut mom = Mom::new(3).unwrap();
165 let out = mom.batch(&[1.0, 2.0, 3.0, 4.0]);
166 out[3].expect("MOM(3) ready after four inputs");
167 assert_eq!(mom.update(f64::NAN), None);
168 assert_eq!(mom.update(f64::INFINITY), None);
169 assert_relative_eq!(mom.update(10.0).unwrap(), 10.0 - 2.0, epsilon = 1e-12);
171 }
172
173 #[test]
174 fn reset_clears_state() {
175 let mut mom = Mom::new(3).unwrap();
176 mom.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
177 assert!(mom.is_ready());
178 mom.reset();
179 assert!(!mom.is_ready());
180 assert_eq!(mom.update(1.0), None);
181 }
182
183 #[test]
184 fn batch_equals_streaming() {
185 let prices: Vec<f64> = (1..=40).map(|i| f64::from(i) * 1.5).collect();
186 let batch = Mom::new(7).unwrap().batch(&prices);
187 let mut b = Mom::new(7).unwrap();
188 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
189 assert_eq!(batch, streamed);
190 }
191}