wickra_core/indicators/
mid_price.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
38pub struct MidPrice {
39 period: usize,
40 candles: VecDeque<Candle>,
41}
42
43impl MidPrice {
44 pub fn new(period: usize) -> Result<Self> {
47 if period == 0 {
48 return Err(Error::PeriodZero);
49 }
50 if period > crate::error::MAX_PERIOD {
51 return Err(Error::InvalidPeriod {
52 message: crate::error::PERIOD_ABOVE_MAX,
53 });
54 }
55 Ok(Self {
56 period,
57 candles: VecDeque::with_capacity(period),
58 })
59 }
60
61 pub const fn period(&self) -> usize {
63 self.period
64 }
65}
66
67impl Indicator for MidPrice {
68 type Input = Candle;
69 type Output = f64;
70
71 #[inline]
72 fn update(&mut self, candle: Candle) -> Option<f64> {
73 if self.candles.len() == self.period {
74 self.candles.pop_front();
75 }
76 self.candles.push_back(candle);
77 if self.candles.len() < self.period {
78 return None;
79 }
80 let highest = self
81 .candles
82 .iter()
83 .map(|c| c.high)
84 .fold(f64::NEG_INFINITY, f64::max);
85 let lowest = self
86 .candles
87 .iter()
88 .map(|c| c.low)
89 .fold(f64::INFINITY, f64::min);
90 Some(f64::midpoint(highest, lowest))
91 }
92
93 fn reset(&mut self) {
94 self.candles.clear();
95 }
96
97 #[inline]
98 fn warmup_period(&self) -> usize {
99 self.period
100 }
101
102 #[inline]
103 fn is_ready(&self) -> bool {
104 self.candles.len() == self.period
105 }
106
107 #[inline]
108 fn name(&self) -> &'static str {
109 "MIDPRICE"
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::traits::BatchExt;
117 use approx::assert_relative_eq;
118
119 fn c(h: f64, l: f64, cl: f64) -> Candle {
120 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
121 }
122
123 #[test]
124 fn rejects_zero_period() {
125 assert!(matches!(MidPrice::new(0), Err(Error::PeriodZero)));
126 }
127
128 #[test]
129 fn accessors_report_config() {
130 let mp = MidPrice::new(7).unwrap();
131 assert_eq!(mp.period(), 7);
132 assert_eq!(mp.name(), "MIDPRICE");
133 assert_eq!(mp.warmup_period(), 7);
134 assert!(!mp.is_ready());
135 }
136
137 #[test]
138 fn averages_window_extremes() {
139 let candles = [c(12.0, 8.0, 10.0), c(14.0, 9.0, 11.0), c(16.0, 10.0, 12.0)];
141 let mut mp = MidPrice::new(3).unwrap();
142 let out: Vec<Option<f64>> = mp.batch(&candles);
143 assert_eq!(out[0], None);
144 assert_eq!(out[1], None);
145 assert_relative_eq!(out[2].unwrap(), 12.0, epsilon = 1e-12);
146 assert!(mp.is_ready());
147 }
148
149 #[test]
150 fn window_slides_and_drops_old_extremes() {
151 let candles = [
153 c(30.0, 10.0, 20.0),
154 c(12.0, 8.0, 10.0),
155 c(14.0, 9.0, 11.0),
156 c(16.0, 10.0, 12.0),
157 ];
158 let mut mp = MidPrice::new(3).unwrap();
159 let out: Vec<Option<f64>> = mp.batch(&candles);
160 assert_relative_eq!(out[3].unwrap(), 12.0, epsilon = 1e-12);
162 }
163
164 #[test]
165 fn reset_clears_state() {
166 let candles = [c(12.0, 8.0, 10.0), c(14.0, 9.0, 11.0), c(16.0, 10.0, 12.0)];
167 let mut mp = MidPrice::new(3).unwrap();
168 let _ = mp.batch(&candles);
169 assert!(mp.is_ready());
170 mp.reset();
171 assert!(!mp.is_ready());
172 assert_eq!(mp.update(candles[0]), None);
173 }
174}