kestrel_chartkit/indicator/
eom.rs1use super::smoothing::Sma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4
5#[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}