Skip to main content

kestrel_chartkit/indicator/
efficiency.rs

1use std::collections::HashMap;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6/// Kaufman Efficiency Ratio: net movement over path length.
7///
8/// Over the last `len + 1` closes, `ER = |close_t - close_{t-len}| / sum(|close_i - close_{i-1}|)`,
9/// and `0` when nothing moved. `1` means the closes went straight from one end to the other, values
10/// near `0` that they travelled far and arrived nowhere. Alerts fire at `>= 0.65` and `<= 0.20`.
11///
12/// First output: with the `len + 1`-th bar. [`Indicator::reset`] clears the window.
13#[derive(Debug, Clone)]
14pub struct LegEfficiencyEngine {
15    len: usize,
16    closes: Vec<f64>,
17    alerts: Vec<IndicatorAlert>,
18}
19
20impl LegEfficiencyEngine {
21    pub fn new(len: usize) -> Self {
22        Self {
23            len,
24            closes: Vec::with_capacity(len + 1),
25            alerts: Vec::new(),
26        }
27    }
28}
29
30impl Indicator for LegEfficiencyEngine {
31    fn name(&self) -> &str {
32        "efficiency"
33    }
34
35    fn warmup_period(&self) -> usize {
36        self.len
37    }
38
39    fn reset(&mut self) {
40        self.closes.clear();
41        self.alerts.clear();
42    }
43
44    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
45        self.closes.push(bar.close);
46        if self.closes.len() > self.len + 1 {
47            self.closes.remove(0);
48        }
49
50        self.alerts.clear();
51
52        if self.closes.len() < self.len + 1 {
53            return None;
54        }
55
56        let change = (self.closes[self.closes.len() - 1] - self.closes[0]).abs();
57        let mut volatility = 0.0f64;
58        for i in 1..self.closes.len() {
59            volatility += (self.closes[i] - self.closes[i - 1]).abs();
60        }
61
62        let er = if volatility > 0.0 {
63            change / volatility
64        } else {
65            0.0
66        };
67
68        if er >= 0.65 {
69            self.alerts.push(IndicatorAlert::new(
70                "high_leg_efficiency",
71                format!(
72                    "High Trend Efficiency Ratio ({:.0}% Clean Move)",
73                    er * 100.0
74                ),
75                0.85,
76            ));
77        } else if er <= 0.20 {
78            self.alerts.push(IndicatorAlert::new(
79                "low_leg_efficiency",
80                format!("High Market Chop / Low Efficiency ({:.0}%)", er * 100.0),
81                0.70,
82            ));
83        }
84
85        Some(IndicatorOutput::new(er))
86    }
87
88    fn alerts(&self) -> Vec<IndicatorAlert> {
89        self.alerts.clone()
90    }
91}
92
93pub fn build_efficiency(params: &HashMap<String, f64>) -> LegEfficiencyEngine {
94    let len = params.get("len").copied().unwrap_or(14.0) as usize;
95    LegEfficiencyEngine::new(len)
96}