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