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