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}
37
38impl MedianMa {
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),
51 })
52 }
53
54 pub const fn period(&self) -> usize {
56 self.period
57 }
58
59 pub fn value(&self) -> Option<f64> {
61 if self.window.len() != self.period {
62 return None;
63 }
64 let mut sorted: Vec<f64> = self.window.iter().copied().collect();
65 sorted.sort_by(|a, b| a.partial_cmp(b).expect("window holds only finite values"));
66 let mid = self.period / 2;
67 if self.period % 2 == 1 {
68 Some(sorted[mid])
69 } else {
70 Some(f64::midpoint(sorted[mid - 1], sorted[mid]))
71 }
72 }
73}
74
75impl Indicator for MedianMa {
76 type Input = f64;
77 type Output = f64;
78
79 fn update(&mut self, input: f64) -> Option<f64> {
80 if !input.is_finite() {
81 return self.value();
82 }
83 if self.window.len() == self.period {
84 self.window.pop_front();
85 }
86 self.window.push_back(input);
87 self.value()
88 }
89
90 fn reset(&mut self) {
91 self.window.clear();
92 }
93
94 fn warmup_period(&self) -> usize {
95 self.period
96 }
97
98 fn is_ready(&self) -> bool {
99 self.window.len() == self.period
100 }
101
102 fn name(&self) -> &'static str {
103 "MedianMA"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 #[test]
114 fn new_rejects_zero_period() {
115 assert!(matches!(MedianMa::new(0), Err(Error::PeriodZero)));
116 }
117
118 #[test]
121 fn accessors_and_metadata() {
122 let mma = MedianMa::new(7).unwrap();
123 assert_eq!(mma.period(), 7);
124 assert_eq!(mma.warmup_period(), 7);
125 assert_eq!(mma.name(), "MedianMA");
126 }
127
128 #[test]
129 fn warmup_returns_none_then_odd_median() {
130 let mut mma = MedianMa::new(3).unwrap();
131 assert_eq!(mma.update(5.0), None);
132 assert_eq!(mma.update(1.0), None);
133 assert_relative_eq!(mma.update(3.0).unwrap(), 3.0, epsilon = 1e-12);
135 }
136
137 #[test]
138 fn even_period_averages_two_central_values() {
139 let mut mma = MedianMa::new(4).unwrap();
141 let v = mma.batch(&[1.0, 2.0, 3.0, 4.0]);
142 assert_relative_eq!(v[3].unwrap(), 2.5, epsilon = 1e-12);
143 }
144
145 #[test]
146 fn robust_to_single_outlier() {
147 let mut mma = MedianMa::new(3).unwrap();
150 let v = mma.batch(&[10.0, 11.0, 9999.0]);
151 assert_relative_eq!(v[2].unwrap(), 11.0, epsilon = 1e-12);
152 }
153
154 #[test]
155 fn period_one_is_pass_through() {
156 let mut mma = MedianMa::new(1).unwrap();
157 assert_relative_eq!(mma.update(5.5).unwrap(), 5.5, epsilon = 1e-12);
158 assert_relative_eq!(mma.update(7.5).unwrap(), 7.5, epsilon = 1e-12);
159 }
160
161 #[test]
162 fn slides_window_correctly() {
163 let mut mma = MedianMa::new(3).unwrap();
165 let v = mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
166 assert_relative_eq!(v[2].unwrap(), 2.0, epsilon = 1e-12);
167 assert_relative_eq!(v[3].unwrap(), 3.0, epsilon = 1e-12);
168 assert_relative_eq!(v[4].unwrap(), 4.0, epsilon = 1e-12);
169 }
170
171 #[test]
172 fn reset_clears_state() {
173 let mut mma = MedianMa::new(4).unwrap();
174 mma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
175 assert!(mma.is_ready());
176 mma.reset();
177 assert!(!mma.is_ready());
178 assert_eq!(mma.update(10.0), None);
179 }
180
181 #[test]
182 fn batch_equals_streaming() {
183 let prices: Vec<f64> = (1..=20).map(|i| (f64::from(i) * 0.7).sin() * 5.0).collect();
184 let mut a = MedianMa::new(5).unwrap();
185 let mut b = MedianMa::new(5).unwrap();
186 assert_eq!(
187 a.batch(&prices),
188 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
189 );
190 }
191
192 #[test]
193 fn ignores_non_finite_input_but_keeps_state() {
194 let mut mma = MedianMa::new(3).unwrap();
195 mma.update(5.0);
196 mma.update(1.0);
197 let ready = mma
198 .update(3.0)
199 .expect("MedianMA(3) ready after three inputs");
200 assert_eq!(mma.update(f64::NAN), Some(ready));
201 assert_eq!(mma.update(f64::INFINITY), Some(ready));
202 assert_relative_eq!(mma.update(8.0).unwrap(), 3.0, epsilon = 1e-12);
204 }
205}