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