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 self.window.len() == self.period {
93 self.window.pop_front();
94 }
95 self.window.push_back(value);
96 if self.window.len() < self.period {
97 return None;
98 }
99 self.scratch.clear();
101 self.scratch.extend(self.window.iter().copied());
102 sort_finite(&mut self.scratch);
103 let med = median_sorted(&self.scratch);
104 for x in &mut self.scratch {
106 *x = (*x - med).abs();
107 }
108 sort_finite(&mut self.scratch);
109 Some(median_sorted(&self.scratch))
110 }
111
112 fn reset(&mut self) {
113 self.window.clear();
114 self.scratch.clear();
115 }
116
117 fn warmup_period(&self) -> usize {
118 self.period
119 }
120
121 fn is_ready(&self) -> bool {
122 self.window.len() == self.period
123 }
124
125 fn name(&self) -> &'static str {
126 "MedianAbsoluteDeviation"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134 use approx::assert_relative_eq;
135
136 #[test]
137 fn rejects_zero_period() {
138 assert!(matches!(
139 MedianAbsoluteDeviation::new(0),
140 Err(Error::PeriodZero)
141 ));
142 }
143
144 #[test]
145 fn accessors_and_metadata() {
146 let m = MedianAbsoluteDeviation::new(14).unwrap();
147 assert_eq!(m.period(), 14);
148 assert_eq!(m.warmup_period(), 14);
149 assert_eq!(m.name(), "MedianAbsoluteDeviation");
150 }
151
152 #[test]
153 fn reference_value() {
154 let mut m = MedianAbsoluteDeviation::new(7).unwrap();
157 let out = m.batch(&[1.0, 1.0, 2.0, 2.0, 4.0, 6.0, 9.0]);
158 assert_relative_eq!(out[6].unwrap(), 1.0, epsilon = 1e-12);
159 }
160
161 #[test]
162 fn constant_series_yields_zero() {
163 let mut m = MedianAbsoluteDeviation::new(5).unwrap();
164 for v in m.batch(&[42.0; 20]).into_iter().flatten() {
165 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
166 }
167 }
168
169 #[test]
170 fn ignores_single_extreme_outlier() {
171 let mut m = MedianAbsoluteDeviation::new(10).unwrap();
175 let mut prices = vec![5.0; 9];
176 prices.push(1_000.0);
177 let last = m.batch(&prices).into_iter().flatten().last().unwrap();
178 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let mut m = MedianAbsoluteDeviation::new(5).unwrap();
184 m.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
185 assert!(m.is_ready());
186 m.reset();
187 assert!(!m.is_ready());
188 assert_eq!(m.update(1.0), None);
189 }
190
191 #[test]
192 fn batch_equals_streaming() {
193 let prices: Vec<f64> = (0..60)
194 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
195 .collect();
196 let batch = MedianAbsoluteDeviation::new(14).unwrap().batch(&prices);
197 let mut b = MedianAbsoluteDeviation::new(14).unwrap();
198 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
199 assert_eq!(batch, streamed);
200 }
201}