1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
42pub struct EaseOfMovement {
43 period: usize,
44 divisor: f64,
45 prev_mid: Option<f64>,
46 window: VecDeque<f64>,
47 sum: RollingSum,
48}
49
50impl EaseOfMovement {
51 pub fn new(period: usize) -> Result<Self> {
56 Self::with_divisor(period, 100_000_000.0)
57 }
58
59 pub fn with_divisor(period: usize, divisor: f64) -> Result<Self> {
68 if period == 0 {
69 return Err(Error::PeriodZero);
70 }
71 if period > crate::error::MAX_PERIOD {
72 return Err(Error::InvalidPeriod {
73 message: crate::error::PERIOD_ABOVE_MAX,
74 });
75 }
76 if !divisor.is_finite() || divisor <= 0.0 {
77 return Err(Error::NonPositiveMultiplier);
78 }
79 Ok(Self {
80 period,
81 divisor,
82 prev_mid: None,
83 window: VecDeque::with_capacity(period),
84 sum: RollingSum::new(),
85 })
86 }
87
88 pub const fn period(&self) -> usize {
90 self.period
91 }
92
93 pub const fn divisor(&self) -> f64 {
95 self.divisor
96 }
97}
98
99impl Indicator for EaseOfMovement {
100 type Input = Candle;
101 type Output = f64;
102
103 #[inline]
104 fn update(&mut self, candle: Candle) -> Option<f64> {
105 let mid = f64::midpoint(candle.high, candle.low);
106 let Some(prev_mid) = self.prev_mid else {
107 self.prev_mid = Some(mid);
109 return None;
110 };
111 let distance = mid - prev_mid;
112 let range = candle.high - candle.low;
113 let emv = if candle.volume == 0.0 {
114 0.0
116 } else {
117 distance * range * self.divisor / candle.volume
118 };
119 self.prev_mid = Some(mid);
120
121 if self.window.len() == self.period {
122 let oldest = self.window.pop_front().expect("non-empty");
123 self.sum.evict(oldest);
124 }
125 self.window.push_back(emv);
126 self.sum.push(emv);
127 if self.sum.needs_reseed(self.period) {
128 self.sum.reseed(self.window.iter().copied());
129 }
130 if self.window.len() < self.period {
131 return None;
132 }
133 Some(self.sum.value() / self.period as f64)
134 }
135
136 fn reset(&mut self) {
137 self.prev_mid = None;
138 self.window.clear();
139 self.sum.reset();
140 }
141
142 #[inline]
143 fn warmup_period(&self) -> usize {
144 self.period + 1
147 }
148
149 #[inline]
150 fn is_ready(&self) -> bool {
151 self.window.len() == self.period
152 }
153
154 #[inline]
155 fn name(&self) -> &'static str {
156 "EaseOfMovement"
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::traits::BatchExt;
164 use approx::assert_relative_eq;
165
166 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
167 Candle::new(open, high, low, close, volume, ts).unwrap()
168 }
169
170 #[test]
171 fn reference_values() {
172 let mut eom = EaseOfMovement::with_divisor(1, 1.0).unwrap();
177 let out = eom.batch(&[
178 candle(9.0, 10.0, 8.0, 9.0, 50.0, 0),
179 candle(12.0, 14.0, 10.0, 12.0, 100.0, 1),
180 ]);
181 assert!(out[0].is_none());
182 assert_relative_eq!(out[1].unwrap(), 0.12, epsilon = 1e-12);
183 }
184
185 #[test]
186 fn rising_midpoints_yield_positive_eom() {
187 let candles: Vec<Candle> = (0..40)
190 .map(|i| {
191 let base = 100.0 + i as f64;
192 candle(base, base + 1.0, base - 1.0, base, 100.0, i)
193 })
194 .collect();
195 let mut eom = EaseOfMovement::new(14).unwrap();
196 for v in eom.batch(&candles).into_iter().flatten() {
197 assert!(v > 0.0, "EOM {v} should be positive on a rising series");
198 }
199 }
200
201 #[test]
202 fn constant_series_yields_zero() {
203 let candles: Vec<Candle> = (0..30)
205 .map(|i| candle(10.0, 11.0, 9.0, 10.0, 50.0, i))
206 .collect();
207 let mut eom = EaseOfMovement::new(10).unwrap();
208 for v in eom.batch(&candles).into_iter().flatten() {
209 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
210 }
211 }
212
213 #[test]
214 fn zero_volume_contributes_zero() {
215 let candles: Vec<Candle> = (0..20)
217 .map(|i| {
218 let base = 100.0 + i as f64;
219 candle(base, base + 1.0, base - 1.0, base, 0.0, i)
220 })
221 .collect();
222 let mut eom = EaseOfMovement::new(10).unwrap();
223 for v in eom.batch(&candles).into_iter().flatten() {
224 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
225 }
226 }
227
228 #[test]
229 fn first_value_on_period_plus_one_candle() {
230 let candles: Vec<Candle> = (0..12)
231 .map(|i| {
232 let base = 100.0 + i as f64;
233 candle(base, base + 1.0, base - 1.0, base, 50.0, i)
234 })
235 .collect();
236 let mut eom = EaseOfMovement::new(5).unwrap();
237 let out = eom.batch(&candles);
238 for (i, v) in out.iter().enumerate().take(5) {
239 assert!(v.is_none(), "index {i} must be None during warmup");
240 }
241 assert!(out[5].is_some(), "first EOM lands at index period");
242 assert_eq!(eom.warmup_period(), 6);
243 }
244
245 #[test]
246 fn rejects_invalid_input() {
247 assert!(EaseOfMovement::new(0).is_err());
248 assert!(EaseOfMovement::with_divisor(14, 0.0).is_err());
249 assert!(EaseOfMovement::with_divisor(14, -1.0).is_err());
250 assert!(EaseOfMovement::with_divisor(14, f64::NAN).is_err());
251 }
252
253 #[test]
257 fn accessors_and_metadata() {
258 let emv = EaseOfMovement::new(14).unwrap();
259 assert_eq!(emv.period(), 14);
260 assert_relative_eq!(emv.divisor(), 100_000_000.0, epsilon = 1e-6);
262 assert_eq!(emv.name(), "EaseOfMovement");
263 }
264
265 #[test]
266 fn reset_clears_state() {
267 let candles: Vec<Candle> = (0..30)
268 .map(|i| {
269 let base = 100.0 + i as f64;
270 candle(base, base + 1.0, base - 1.0, base, 50.0, i)
271 })
272 .collect();
273 let mut eom = EaseOfMovement::new(10).unwrap();
274 eom.batch(&candles);
275 assert!(eom.is_ready());
276 eom.reset();
277 assert!(!eom.is_ready());
278 assert_eq!(eom.update(candles[0]), None);
279 }
280
281 #[test]
282 fn batch_equals_streaming() {
283 let candles: Vec<Candle> = (0..80)
284 .map(|i| {
285 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
286 candle(
287 mid,
288 mid + 2.0,
289 mid - 2.0,
290 mid + 0.5,
291 10.0 + (i % 5) as f64,
292 i,
293 )
294 })
295 .collect();
296 let mut a = EaseOfMovement::new(14).unwrap();
297 let mut b = EaseOfMovement::new(14).unwrap();
298 assert_eq!(
299 a.batch(&candles),
300 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
301 );
302 }
303}