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        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    /// Configured period.
57    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        // Window {8, 12, 10}: highest 12, lowest 8 -> 10.
130        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        // After the 30 spike leaves the window, the midpoint falls back.
141        let mut mp = MidPoint::new(3).unwrap();
142        let out: Vec<Option<f64>> = mp.batch(&[30.0, 8.0, 12.0, 10.0]);
143        // Last window {8, 12, 10}: (12 + 8) / 2 = 10.
144        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}