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