wickra_core/indicators/
mid_point.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
33pub struct MidPoint {
34 period: usize,
35 window: VecDeque<f64>,
36}
37
38impl MidPoint {
39 pub fn new(period: usize) -> Result<Self> {
42 if period == 0 {
43 return Err(Error::PeriodZero);
44 }
45 if period > crate::error::MAX_PERIOD {
46 return Err(Error::InvalidPeriod {
47 message: crate::error::PERIOD_ABOVE_MAX,
48 });
49 }
50 Ok(Self {
51 period,
52 window: VecDeque::with_capacity(period),
53 })
54 }
55
56 pub const fn period(&self) -> usize {
58 self.period
59 }
60}
61
62impl Indicator for MidPoint {
63 type Input = f64;
64 type Output = f64;
65
66 #[inline]
67 fn update(&mut self, value: f64) -> Option<f64> {
68 if !value.is_finite() {
69 return None;
70 }
71 if self.window.len() == self.period {
72 self.window.pop_front();
73 }
74 self.window.push_back(value);
75 if self.window.len() < self.period {
76 return None;
77 }
78 let highest = self
79 .window
80 .iter()
81 .copied()
82 .fold(f64::NEG_INFINITY, f64::max);
83 let lowest = self.window.iter().copied().fold(f64::INFINITY, f64::min);
84 Some(f64::midpoint(highest, lowest))
85 }
86
87 fn reset(&mut self) {
88 self.window.clear();
89 }
90
91 #[inline]
92 fn warmup_period(&self) -> usize {
93 self.period
94 }
95
96 #[inline]
97 fn is_ready(&self) -> bool {
98 self.window.len() == self.period
99 }
100
101 #[inline]
102 fn name(&self) -> &'static str {
103 "MIDPOINT"
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 rejects_zero_period() {
115 assert!(matches!(MidPoint::new(0), Err(Error::PeriodZero)));
116 }
117
118 #[test]
119 fn accessors_report_config() {
120 let mp = MidPoint::new(7).unwrap();
121 assert_eq!(mp.period(), 7);
122 assert_eq!(mp.name(), "MIDPOINT");
123 assert_eq!(mp.warmup_period(), 7);
124 assert!(!mp.is_ready());
125 }
126
127 #[test]
128 fn averages_window_min_and_max() {
129 let mut mp = MidPoint::new(3).unwrap();
131 let out: Vec<Option<f64>> = mp.batch(&[8.0, 12.0, 10.0]);
132 assert_eq!(out[0], None);
133 assert_eq!(out[1], None);
134 assert_relative_eq!(out[2].unwrap(), 10.0, epsilon = 1e-12);
135 assert!(mp.is_ready());
136 }
137
138 #[test]
139 fn window_slides_and_drops_old_values() {
140 let mut mp = MidPoint::new(3).unwrap();
142 let out: Vec<Option<f64>> = mp.batch(&[30.0, 8.0, 12.0, 10.0]);
143 assert_relative_eq!(out[3].unwrap(), 10.0, epsilon = 1e-12);
145 }
146
147 #[test]
148 fn reset_clears_state() {
149 let mut mp = MidPoint::new(3).unwrap();
150 let _ = mp.batch(&[8.0, 12.0, 10.0]);
151 assert!(mp.is_ready());
152 mp.reset();
153 assert!(!mp.is_ready());
154 assert_eq!(mp.update(8.0), None);
155 }
156}