Skip to main content

wickra_core/indicators/
mid_point.rs

1//! Midpoint (MIDPOINT) over a rolling window of a scalar series.
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8/// Midpoint (`MIDPOINT`): the average of the highest and lowest value of the
9/// input series over the last `period` points.
10///
11/// ```text
12/// MIDPOINT = (highest(value, period) + lowest(value, period)) / 2
13/// ```
14///
15/// Where [`MidPrice`](crate::MidPrice) takes the window extremes from a candle's
16/// high/low, `MIDPOINT` works on a single scalar stream (typically the close),
17/// taking the max and min of that stream over the window. The first value is
18/// emitted once `period` points have been seen.
19///
20/// # Example
21///
22/// ```
23/// use wickra_core::{Indicator, MidPoint};
24///
25/// let mut indicator = MidPoint::new(5).unwrap();
26/// let mut last = None;
27/// for i in 0..40 {
28///     last = indicator.update(100.0 + f64::from(i));
29/// }
30/// assert!(last.is_some());
31/// ```
32#[derive(Debug, Clone)]
33pub struct MidPoint {
34    period: usize,
35    window: VecDeque<f64>,
36}
37
38impl MidPoint {
39    /// # Errors
40    /// Returns [`Error::PeriodZero`] if `period == 0`.
41    pub fn new(period: usize) -> Result<Self> {
42        if period == 0 {
43            return Err(Error::PeriodZero);
44        }
45        Ok(Self {
46            period,
47            window: VecDeque::with_capacity(period),
48        })
49    }
50
51    /// Configured period.
52    pub const fn period(&self) -> usize {
53        self.period
54    }
55}
56
57impl Indicator for MidPoint {
58    type Input = f64;
59    type Output = f64;
60
61    fn update(&mut self, value: f64) -> Option<f64> {
62        if !value.is_finite() {
63            return None;
64        }
65        if self.window.len() == self.period {
66            self.window.pop_front();
67        }
68        self.window.push_back(value);
69        if self.window.len() < self.period {
70            return None;
71        }
72        let highest = self
73            .window
74            .iter()
75            .copied()
76            .fold(f64::NEG_INFINITY, f64::max);
77        let lowest = self.window.iter().copied().fold(f64::INFINITY, f64::min);
78        Some(f64::midpoint(highest, lowest))
79    }
80
81    fn reset(&mut self) {
82        self.window.clear();
83    }
84
85    fn warmup_period(&self) -> usize {
86        self.period
87    }
88
89    fn is_ready(&self) -> bool {
90        self.window.len() == self.period
91    }
92
93    fn name(&self) -> &'static str {
94        "MIDPOINT"
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::traits::BatchExt;
102    use approx::assert_relative_eq;
103
104    #[test]
105    fn rejects_zero_period() {
106        assert!(matches!(MidPoint::new(0), Err(Error::PeriodZero)));
107    }
108
109    #[test]
110    fn accessors_report_config() {
111        let mp = MidPoint::new(7).unwrap();
112        assert_eq!(mp.period(), 7);
113        assert_eq!(mp.name(), "MIDPOINT");
114        assert_eq!(mp.warmup_period(), 7);
115        assert!(!mp.is_ready());
116    }
117
118    #[test]
119    fn averages_window_min_and_max() {
120        // Window {8, 12, 10}: highest 12, lowest 8 -> 10.
121        let mut mp = MidPoint::new(3).unwrap();
122        let out: Vec<Option<f64>> = mp.batch(&[8.0, 12.0, 10.0]);
123        assert_eq!(out[0], None);
124        assert_eq!(out[1], None);
125        assert_relative_eq!(out[2].unwrap(), 10.0, epsilon = 1e-12);
126        assert!(mp.is_ready());
127    }
128
129    #[test]
130    fn window_slides_and_drops_old_values() {
131        // After the 30 spike leaves the window, the midpoint falls back.
132        let mut mp = MidPoint::new(3).unwrap();
133        let out: Vec<Option<f64>> = mp.batch(&[30.0, 8.0, 12.0, 10.0]);
134        // Last window {8, 12, 10}: (12 + 8) / 2 = 10.
135        assert_relative_eq!(out[3].unwrap(), 10.0, epsilon = 1e-12);
136    }
137
138    #[test]
139    fn reset_clears_state() {
140        let mut mp = MidPoint::new(3).unwrap();
141        let _ = mp.batch(&[8.0, 12.0, 10.0]);
142        assert!(mp.is_ready());
143        mp.reset();
144        assert!(!mp.is_ready());
145        assert_eq!(mp.update(8.0), None);
146    }
147}