wickra_core/indicators/
median_absolute_deviation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
42pub struct MedianAbsoluteDeviation {
43 period: usize,
44 window: VecDeque<f64>,
45 scratch: Vec<f64>,
47}
48
49impl MedianAbsoluteDeviation {
50 pub fn new(period: usize) -> Result<Self> {
55 if period == 0 {
56 return Err(Error::PeriodZero);
57 }
58 if period > crate::error::MAX_PERIOD {
59 return Err(Error::InvalidPeriod {
60 message: crate::error::PERIOD_ABOVE_MAX,
61 });
62 }
63 Ok(Self {
64 period,
65 window: VecDeque::with_capacity(period),
66 scratch: Vec::with_capacity(period),
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74}
75
76fn sort_finite(buf: &mut [f64]) {
78 buf.sort_by(f64::total_cmp);
79}
80
81fn median_sorted(sorted: &[f64]) -> f64 {
83 let n = sorted.len();
84 let mid = n / 2;
85 if n % 2 == 0 {
86 f64::midpoint(sorted[mid - 1], sorted[mid])
87 } else {
88 sorted[mid]
89 }
90}
91
92impl Indicator for MedianAbsoluteDeviation {
93 type Input = f64;
94 type Output = f64;
95
96 #[inline]
97 fn update(&mut self, value: f64) -> Option<f64> {
98 if !value.is_finite() {
99 return None;
100 }
101 if self.window.len() == self.period {
102 self.window.pop_front();
103 }
104 self.window.push_back(value);
105 if self.window.len() < self.period {
106 return None;
107 }
108 self.scratch.clear();
110 self.scratch.extend(self.window.iter().copied());
111 sort_finite(&mut self.scratch);
112 let med = median_sorted(&self.scratch);
113 for x in &mut self.scratch {
115 *x = (*x - med).abs();
116 }
117 sort_finite(&mut self.scratch);
118 Some(median_sorted(&self.scratch))
119 }
120
121 fn reset(&mut self) {
122 self.window.clear();
123 self.scratch.clear();
124 }
125
126 #[inline]
127 fn warmup_period(&self) -> usize {
128 self.period
129 }
130
131 #[inline]
132 fn is_ready(&self) -> bool {
133 self.window.len() == self.period
134 }
135
136 #[inline]
137 fn name(&self) -> &'static str {
138 "MedianAbsoluteDeviation"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::BatchExt;
146 use approx::assert_relative_eq;
147
148 #[test]
149 fn rejects_zero_period() {
150 assert!(matches!(
151 MedianAbsoluteDeviation::new(0),
152 Err(Error::PeriodZero)
153 ));
154 }
155
156 #[test]
157 fn accessors_and_metadata() {
158 let m = MedianAbsoluteDeviation::new(14).unwrap();
159 assert_eq!(m.period(), 14);
160 assert_eq!(m.warmup_period(), 14);
161 assert_eq!(m.name(), "MedianAbsoluteDeviation");
162 }
163
164 #[test]
165 fn reference_value() {
166 let mut m = MedianAbsoluteDeviation::new(7).unwrap();
169 let out = m.batch(&[1.0, 1.0, 2.0, 2.0, 4.0, 6.0, 9.0]);
170 assert_relative_eq!(out[6].unwrap(), 1.0, epsilon = 1e-12);
171 }
172
173 #[test]
174 fn constant_series_yields_zero() {
175 let mut m = MedianAbsoluteDeviation::new(5).unwrap();
176 for v in m.batch(&[42.0; 20]).into_iter().flatten() {
177 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
178 }
179 }
180
181 #[test]
182 fn ignores_single_extreme_outlier() {
183 let mut m = MedianAbsoluteDeviation::new(10).unwrap();
187 let mut prices = vec![5.0; 9];
188 prices.push(1_000.0);
189 let last = m.batch(&prices).into_iter().flatten().last().unwrap();
190 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
191 }
192
193 #[test]
194 fn reset_clears_state() {
195 let mut m = MedianAbsoluteDeviation::new(5).unwrap();
196 m.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
197 assert!(m.is_ready());
198 m.reset();
199 assert!(!m.is_ready());
200 assert_eq!(m.update(1.0), None);
201 }
202
203 #[test]
204 fn batch_equals_streaming() {
205 let prices: Vec<f64> = (0..60)
206 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
207 .collect();
208 let batch = MedianAbsoluteDeviation::new(14).unwrap().batch(&prices);
209 let mut b = MedianAbsoluteDeviation::new(14).unwrap();
210 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
211 assert_eq!(batch, streamed);
212 }
213}