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 self.window.len() == self.period {
63            self.window.pop_front();
64        }
65        self.window.push_back(value);
66        if self.window.len() < self.period {
67            return None;
68        }
69        let highest = self
70            .window
71            .iter()
72            .copied()
73            .fold(f64::NEG_INFINITY, f64::max);
74        let lowest = self.window.iter().copied().fold(f64::INFINITY, f64::min);
75        Some(f64::midpoint(highest, lowest))
76    }
77
78    fn reset(&mut self) {
79        self.window.clear();
80    }
81
82    fn warmup_period(&self) -> usize {
83        self.period
84    }
85
86    fn is_ready(&self) -> bool {
87        self.window.len() == self.period
88    }
89
90    fn name(&self) -> &'static str {
91        "MIDPOINT"
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::traits::BatchExt;
99    use approx::assert_relative_eq;
100
101    #[test]
102    fn rejects_zero_period() {
103        assert!(matches!(MidPoint::new(0), Err(Error::PeriodZero)));
104    }
105
106    #[test]
107    fn accessors_report_config() {
108        let mp = MidPoint::new(7).unwrap();
109        assert_eq!(mp.period(), 7);
110        assert_eq!(mp.name(), "MIDPOINT");
111        assert_eq!(mp.warmup_period(), 7);
112        assert!(!mp.is_ready());
113    }
114
115    #[test]
116    fn averages_window_min_and_max() {
117        // Window {8, 12, 10}: highest 12, lowest 8 -> 10.
118        let mut mp = MidPoint::new(3).unwrap();
119        let out: Vec<Option<f64>> = mp.batch(&[8.0, 12.0, 10.0]);
120        assert_eq!(out[0], None);
121        assert_eq!(out[1], None);
122        assert_relative_eq!(out[2].unwrap(), 10.0, epsilon = 1e-12);
123        assert!(mp.is_ready());
124    }
125
126    #[test]
127    fn window_slides_and_drops_old_values() {
128        // After the 30 spike leaves the window, the midpoint falls back.
129        let mut mp = MidPoint::new(3).unwrap();
130        let out: Vec<Option<f64>> = mp.batch(&[30.0, 8.0, 12.0, 10.0]);
131        // Last window {8, 12, 10}: (12 + 8) / 2 = 10.
132        assert_relative_eq!(out[3].unwrap(), 10.0, epsilon = 1e-12);
133    }
134
135    #[test]
136    fn reset_clears_state() {
137        let mut mp = MidPoint::new(3).unwrap();
138        let _ = mp.batch(&[8.0, 12.0, 10.0]);
139        assert!(mp.is_ready());
140        mp.reset();
141        assert!(!mp.is_ready());
142        assert_eq!(mp.update(8.0), None);
143    }
144}