quant_indicators/
efficiency_ratio.rs1use quant_primitives::Candle;
19use rust_decimal::Decimal;
20
21use crate::error::IndicatorError;
22use crate::indicator::Indicator;
23use crate::series::Series;
24
25#[derive(Debug, Clone)]
27pub struct EfficiencyRatio {
28 lookback: usize,
29 name: String,
30}
31
32impl EfficiencyRatio {
33 pub fn new(lookback: usize) -> Result<Self, IndicatorError> {
39 if lookback == 0 {
40 return Err(IndicatorError::InvalidParameter {
41 message: "EfficiencyRatio lookback must be >= 1, got 0".to_string(),
42 });
43 }
44 Ok(Self {
45 lookback,
46 name: format!("ER({})", lookback),
47 })
48 }
49
50 pub fn compute_ratio(&self, candles: &[Candle]) -> Result<Decimal, IndicatorError> {
54 let min_required = self.lookback + 1;
55 if candles.len() < min_required {
56 return Err(IndicatorError::InsufficientData {
57 required: min_required,
58 actual: candles.len(),
59 });
60 }
61
62 let end = candles.len() - 1;
63 let start = end - self.lookback;
64 let closes: Vec<Decimal> = candles[start..=end].iter().map(|c| c.close()).collect();
65 Self::er_from_closes(&closes)
66 }
67
68 pub fn compute_from_closes(&self, closes: &[Decimal]) -> Result<Decimal, IndicatorError> {
73 let min_required = self.lookback + 1;
74 if closes.len() < min_required {
75 return Err(IndicatorError::InsufficientData {
76 required: min_required,
77 actual: closes.len(),
78 });
79 }
80
81 let end = closes.len() - 1;
82 let start = end - self.lookback;
83 Self::er_from_closes(&closes[start..=end])
84 }
85
86 fn er_from_closes(closes: &[Decimal]) -> Result<Decimal, IndicatorError> {
90 let net = (closes[closes.len() - 1] - closes[0]).abs();
91
92 let mut path = Decimal::ZERO;
93 for j in 1..closes.len() {
94 path += (closes[j] - closes[j - 1]).abs();
95 }
96
97 if path.is_zero() {
98 return Ok(Decimal::ZERO);
99 }
100
101 Ok(net / path)
102 }
103
104 fn er_at(candles: &[Candle], start: usize, end: usize) -> Result<Decimal, IndicatorError> {
106 let window = &candles[start..=end];
107 let net = (window[window.len() - 1].close() - window[0].close()).abs();
108
109 let mut path = Decimal::ZERO;
110 for j in 1..window.len() {
111 path += (window[j].close() - window[j - 1].close()).abs();
112 }
113
114 if path.is_zero() {
115 return Ok(Decimal::ZERO);
116 }
117
118 Ok(net / path)
119 }
120
121 #[must_use]
123 pub fn lookback(&self) -> usize {
124 self.lookback
125 }
126}
127
128#[cfg(test)]
129#[path = "efficiency_ratio_tests.rs"]
130mod tests;
131
132impl Indicator for EfficiencyRatio {
133 fn name(&self) -> &str {
134 &self.name
135 }
136
137 fn warmup_period(&self) -> usize {
138 self.lookback + 1
139 }
140
141 fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
142 let min_required = self.lookback + 1;
143 if candles.len() < min_required {
144 return Err(IndicatorError::InsufficientData {
145 required: min_required,
146 actual: candles.len(),
147 });
148 }
149
150 let mut values = Vec::with_capacity(candles.len() - self.lookback);
151 for i in self.lookback..candles.len() {
152 let start = i - self.lookback;
153 let er = Self::er_at(candles, start, i)?;
154 values.push((candles[i].timestamp(), er));
155 }
156
157 Ok(Series::new(values))
158 }
159}