wickra_core/indicators/
median_ma.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
33pub struct MedianMa {
34 period: usize,
35 window: VecDeque<f64>,
36 scratch: Vec<f64>,
38 last: Option<f64>,
40}
41
42impl MedianMa {
43 pub fn new(period: usize) -> Result<Self> {
49 if period == 0 {
50 return Err(Error::PeriodZero);
51 }
52 if period > crate::error::MAX_PERIOD {
53 return Err(Error::InvalidPeriod {
54 message: crate::error::PERIOD_ABOVE_MAX,
55 });
56 }
57 Ok(Self {
58 period,
59 window: VecDeque::with_capacity(period),
60 scratch: Vec::with_capacity(period),
61 last: None,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69
70 pub const fn value(&self) -> Option<f64> {
75 self.last
76 }
77
78 fn recompute(&mut self) {
80 if self.window.len() != self.period {
81 self.last = None;
82 return;
83 }
84 self.scratch.clear();
85 self.scratch.extend(self.window.iter().copied());
86 self.scratch.sort_unstable_by(f64::total_cmp);
89 let mid = self.period / 2;
90 self.last = Some(if self.period % 2 == 1 {
91 self.scratch[mid]
92 } else {
93 f64::midpoint(self.scratch[mid - 1], self.scratch[mid])
94 });
95 }
96}
97
98impl Indicator for MedianMa {
99 type Input = f64;
100 type Output = f64;
101
102 #[inline]
103 fn update(&mut self, input: f64) -> Option<f64> {
104 if !input.is_finite() {
105 return None;
106 }
107 if self.window.len() == self.period {
108 self.window.pop_front();
109 }
110 self.window.push_back(input);
111 self.recompute();
112 self.last
113 }
114
115 fn reset(&mut self) {
116 self.window.clear();
117 self.scratch.clear();
118 self.last = None;
119 }
120
121 #[inline]
122 fn warmup_period(&self) -> usize {
123 self.period
124 }
125
126 #[inline]
127 fn is_ready(&self) -> bool {
128 self.window.len() == self.period
129 }
130
131 #[inline]
132 fn name(&self) -> &'static str {
133 "MedianMA"
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::traits::BatchExt;
141 use approx::assert_relative_eq;
142
143 #[test]
144 fn new_rejects_zero_period() {
145 assert!(matches!(MedianMa::new(0), Err(Error::PeriodZero)));
146 }
147
148 #[test]
151 fn accessors_and_metadata() {
152 let mma = MedianMa::new(7).unwrap();
153 assert_eq!(mma.period(), 7);
154 assert_eq!(mma.warmup_period(), 7);
155 assert_eq!(mma.name(), "MedianMA");
156 }
157
158 #[test]
159 fn warmup_returns_none_then_odd_median() {
160 let mut mma = MedianMa::new(3).unwrap();
161 assert_eq!(mma.update(5.0), None);
162 assert_eq!(mma.update(1.0), None);
163 assert_relative_eq!(mma.update(3.0).unwrap(), 3.0, epsilon = 1e-12);
165 }
166
167 #[test]
168 fn even_period_averages_two_central_values() {
169 let mut mma = MedianMa::new(4).unwrap();
171 let v = mma.batch(&[1.0, 2.0, 3.0, 4.0]);
172 assert_relative_eq!(v[3].unwrap(), 2.5, epsilon = 1e-12);
173 }
174
175 #[test]
176 fn robust_to_single_outlier() {
177 let mut mma = MedianMa::new(3).unwrap();
180 let v = mma.batch(&[10.0, 11.0, 9999.0]);
181 assert_relative_eq!(v[2].unwrap(), 11.0, epsilon = 1e-12);
182 }
183
184 #[test]
185 fn period_one_is_pass_through() {
186 let mut mma = MedianMa::new(1).unwrap();
187 assert_relative_eq!(mma.update(5.5).unwrap(), 5.5, epsilon = 1e-12);
188 assert_relative_eq!(mma.update(7.5).unwrap(), 7.5, epsilon = 1e-12);
189 }
190
191 #[test]
192 fn slides_window_correctly() {
193 let mut mma = MedianMa::new(3).unwrap();
195 let v = mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
196 assert_relative_eq!(v[2].unwrap(), 2.0, epsilon = 1e-12);
197 assert_relative_eq!(v[3].unwrap(), 3.0, epsilon = 1e-12);
198 assert_relative_eq!(v[4].unwrap(), 4.0, epsilon = 1e-12);
199 }
200
201 #[test]
202 fn reset_clears_state() {
203 let mut mma = MedianMa::new(4).unwrap();
204 mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
205 assert!(mma.is_ready());
206 mma.reset();
207 assert!(!mma.is_ready());
208 assert_eq!(mma.update(10.0), None);
209 }
210
211 #[test]
212 fn batch_equals_streaming() {
213 let prices: Vec<f64> = (1..=20).map(|i| (f64::from(i) * 0.7).sin() * 5.0).collect();
214 let mut a = MedianMa::new(5).unwrap();
215 let mut b = MedianMa::new(5).unwrap();
216 assert_eq!(
217 a.batch(&prices),
218 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
219 );
220 }
221
222 #[test]
223 fn ignores_non_finite_input_but_keeps_state() {
224 let mut mma = MedianMa::new(3).unwrap();
225 mma.update(5.0);
226 mma.update(1.0);
227 let _ready = mma
228 .update(3.0)
229 .expect("MedianMA(3) ready after three inputs");
230 assert_eq!(mma.update(f64::NAN), None);
231 assert_eq!(mma.update(f64::INFINITY), None);
232 assert_relative_eq!(mma.update(8.0).unwrap(), 3.0, epsilon = 1e-12);
234 }
235}