1use super::{clamp_param, over_finite_bars};
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8#[derive(Clone)]
18pub struct EaseOfMovement {
19 period: usize,
21 divisor: f64,
23 values: IndicatorValues,
24 color: Color32,
25 visible: bool,
26}
27
28impl EaseOfMovement {
29 pub fn new(period: usize) -> Self {
31 Self {
32 period: period.max(1),
33 divisor: 10_000.0,
34 values: IndicatorValues::new(),
35 color: palette::DARK_GREEN,
36 visible: true,
37 }
38 }
39
40 pub fn with_divisor(mut self, divisor: f64) -> Self {
42 self.divisor = clamp_param(divisor, f64::EPSILON, f64::MAX);
43 self
44 }
45
46 pub fn with_color(mut self, color: Color32) -> Self {
48 self.color = color;
49 self
50 }
51}
52
53impl Default for EaseOfMovement {
55 fn default() -> Self {
56 Self::new(14)
57 }
58}
59
60impl EaseOfMovement {
61 fn compute(&self, data: &[Bar]) -> IndicatorValues {
63 let raw: Vec<Option<f64>> = data
65 .windows(2)
66 .map(|pair| {
67 let (prev, current) = (&pair[0], &pair[1]);
68 let distance_moved =
70 (current.high + current.low) / 2.0 - (prev.high + prev.low) / 2.0;
71 let range = current.high - current.low;
72 if range == 0.0 {
73 Some(0.0)
74 } else {
75 let box_ratio = (current.volume / self.divisor) / range;
77 Some(distance_moved / box_ratio).filter(|eom| eom.is_finite())
78 }
79 })
80 .collect();
81
82 (0..data.len())
84 .map(|i| {
85 let Some(window) = i.checked_sub(self.period).map(|start| &raw[start..i]) else {
86 return IndicatorValue::None;
87 };
88 let defined = window.iter().flatten();
89 match defined.clone().count() {
90 0 => IndicatorValue::None,
91 n => IndicatorValue::Single(defined.sum::<f64>() / n as f64),
92 }
93 })
94 .collect()
95 }
96}
97
98impl Indicator for EaseOfMovement {
99 crate::studies::inputs::study_inputs!(self;
100 period: length("Period", 1, usize::MAX),
101 divisor: number("Divisor", f64::EPSILON, f64::MAX)
102 ; {});
103
104 fn name(&self) -> &str {
105 "EoM"
106 }
107
108 fn desc(&self) -> &str {
109 "Ease of Movement - Relates price change to volume"
110 }
111
112 fn calculate(&mut self, data: &[Bar]) {
113 self.update(data, 0);
114 }
115
116 fn update(&mut self, data: &[Bar], from: usize) {
117 let mut values = std::mem::take(&mut self.values);
118 update_window(
119 &mut values,
120 data,
121 from,
122 usable_behind(self.period, |b| {
123 b.high.is_finite() && b.low.is_finite() && b.volume.is_finite()
124 }),
125 |data| {
126 over_finite_bars(
127 data,
128 |b| b.high.is_finite() && b.low.is_finite() && b.volume.is_finite(),
129 |data| self.compute(data),
130 )
131 },
132 );
133 self.values = values;
134 }
135
136 fn is_incremental(&self) -> bool {
137 true
138 }
139
140 fn values(&self) -> &IndicatorValues {
141 &self.values
142 }
143
144 fn colors(&self) -> Vec<Color32> {
145 vec![self.color]
146 }
147
148 fn set_colors(&mut self, colors: Vec<Color32>) {
149 if !colors.is_empty() {
150 self.color = colors[0];
151 }
152 }
153
154 fn is_overlay(&self) -> bool {
155 false
156 }
157
158 fn is_visible(&self) -> bool {
159 self.visible
160 }
161
162 fn set_visible(&mut self, visible: bool) {
163 self.visible = visible;
164 }
165
166 fn clone_box(&self) -> Box<dyn Indicator> {
167 Box::new(self.clone())
168 }
169
170 fn line_names(&self) -> Vec<String> {
171 vec![format!("EoM({})", self.period)]
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use chrono::Utc;
179
180 fn make_bar(high: f64, low: f64, close: f64, volume: f64) -> Bar {
181 Bar {
182 time: Utc::now(),
183 open: (high + low) / 2.0,
184 high,
185 low,
186 close,
187 volume,
188 colors: None,
189 payload: None,
190 }
191 }
192
193 #[test]
197 fn default_divisor_matches_reference() {
198 let example = [(75.10, 74.10, 584_800.0), (75.80, 74.17, 984_031.0)];
200 let trend = [
201 example[0],
202 example[1],
203 (76.20, 75.00, 700_000.0),
204 (75.90, 74.60, 1_200_000.0),
205 (77.00, 75.50, 900_000.0),
206 (77.40, 76.10, 650_000.0),
207 ];
208 let flat = [(100.0, 100.0, 1_000.0); 4];
209 let zero_volume = [
210 (10.5, 9.5, 1_000.0),
211 (12.0, 10.0, 0.0),
212 (13.0, 11.0, 1_000.0),
213 ];
214 let zero_volume_windows = [
216 (10.0, 9.0, 50_000.0),
217 (11.0, 10.0, 40_000.0),
218 (12.0, 10.0, 0.0),
219 (12.5, 11.5, 60_000.0),
220 (12.0, 11.0, 0.0),
221 (13.0, 12.0, 0.0),
222 (13.0, 12.0, 0.0),
223 (14.0, 13.0, 30_000.0),
224 (14.0, 14.0, 0.0),
225 (15.0, 14.0, 20_000.0),
226 ];
227 let non_finite = [
229 (10.0, 9.0, 50_000.0),
230 (11.0, 10.0, 40_000.0),
231 (f64::NAN, f64::NAN, 30_000.0),
232 (13.0, 12.0, 20_000.0),
233 (14.0, 13.0, 10_000.0),
234 ];
235 let cases = [
236 (
237 "worked example",
238 &example[..],
239 1,
240 vec![None, Some(0.00637733973828067)],
241 ),
242 (
243 "trend",
244 &trend[..],
245 3,
246 vec![
247 None,
248 None,
249 None,
250 Some(0.004376176738157037),
251 Some(0.007805952380952369),
252 Some(0.007625000000000039),
253 ],
254 ),
255 ("flat", &flat[..], 2, vec![None, None, Some(0.0), Some(0.0)]),
256 (
257 "zero volume",
258 &zero_volume[..],
259 1,
260 vec![None, None, Some(20.0)],
261 ),
262 (
263 "zero volume windows",
264 &zero_volume_windows[..],
265 3,
266 vec![
267 None,
268 None,
269 None,
270 Some(0.20833333333333334),
271 Some(0.16666666666666666),
272 Some(0.16666666666666666),
273 None,
274 Some(0.3333333333333333),
275 Some(0.16666666666666666),
276 Some(0.19444444444444445),
277 ],
278 ),
279 (
280 "non-finite bar",
281 &non_finite[..],
282 2,
283 vec![None, None, None, Some(0.625), Some(1.0)],
284 ),
285 ];
286
287 for (name, bars, period, expected) in cases {
288 let data: Vec<Bar> = bars
289 .iter()
290 .map(|&(h, l, v)| make_bar(h, l, (h + l) / 2.0, v))
291 .collect();
292 let mut eom = EaseOfMovement::new(period);
293 eom.calculate(&data);
294 assert_eq!(eom.values.len(), expected.len(), "{name}");
295 for (i, (got, want)) in eom.values.to_rows().iter().zip(expected.iter()).enumerate() {
296 match (got, want) {
297 (IndicatorValue::None, None) => {}
298 (IndicatorValue::Single(g), Some(w)) => {
299 assert!((g - w).abs() <= 1e-12, "{name}[{i}]: got {g}, want {w}")
300 }
301 _ => panic!("{name}[{i}]: got {got:?}, want {want:?}"),
302 }
303 }
304 }
305 }
306}