Skip to main content

kestrel_chartkit/indicator/
eom.rs

1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4
5/// Ease of Movement (EOM) Engine.
6/// EOM = SMA( (Midpoint_t - Midpoint_{t-1}) / (Volume / (High - Low)), period )
7#[derive(Debug, Clone)]
8pub struct EomEngine {
9    period: usize,
10    volume_divisor: f64,
11    prev_hl2: Option<f64>,
12    sma: Sma,
13}
14
15impl EomEngine {
16    pub fn new(period: usize, volume_divisor: f64) -> Self {
17        Self {
18            period: period.max(1),
19            volume_divisor: volume_divisor.max(1.0),
20            prev_hl2: None,
21            sma: Sma::new(period),
22        }
23    }
24
25    pub fn with_defaults() -> Self {
26        Self::new(14, 10000.0)
27    }
28}
29
30impl Indicator for EomEngine {
31    fn name(&self) -> &str {
32        "eom"
33    }
34
35    fn warmup_period(&self) -> usize {
36        self.period + 1
37    }
38
39    fn reset(&mut self) {
40        self.prev_hl2 = None;
41        self.sma.reset();
42    }
43
44    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
45        let hl2 = (bar.high + bar.low) / 2.0;
46
47        let prev = match self.prev_hl2 {
48            Some(p) => p,
49            None => {
50                self.prev_hl2 = Some(hl2);
51                return None;
52            }
53        };
54        self.prev_hl2 = Some(hl2);
55
56        let distance = hl2 - prev;
57        let range = (bar.high - bar.low).max(1e-8);
58        let box_ratio = (bar.volume / self.volume_divisor) / range;
59
60        let raw_eom = if box_ratio > 1e-8 {
61            distance / box_ratio
62        } else {
63            0.0
64        };
65        let eom_val = self.sma.update(raw_eom)?;
66
67        Some(IndicatorOutput::new(eom_val))
68    }
69
70    fn alerts(&self) -> Vec<IndicatorAlert> {
71        Vec::new()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_eom_basic() {
81        let mut eom = EomEngine::with_defaults();
82        let mut out = None;
83        for i in 0..20 {
84            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 10000.0);
85            out = eom.on_bar(&b);
86        }
87        assert!(out.is_some());
88    }
89}