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