use quant_primitives::Candle;
use rust_decimal::Decimal;
use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;
#[derive(Debug, Clone)]
pub struct EfficiencyRatio {
lookback: usize,
name: String,
}
impl EfficiencyRatio {
pub fn new(lookback: usize) -> Result<Self, IndicatorError> {
if lookback == 0 {
return Err(IndicatorError::InvalidParameter {
message: "EfficiencyRatio lookback must be >= 1, got 0".to_string(),
});
}
Ok(Self {
lookback,
name: format!("ER({})", lookback),
})
}
pub fn compute_ratio(&self, candles: &[Candle]) -> Result<Decimal, IndicatorError> {
let min_required = self.lookback + 1;
if candles.len() < min_required {
return Err(IndicatorError::InsufficientData {
required: min_required,
actual: candles.len(),
});
}
let end = candles.len() - 1;
let start = end - self.lookback;
let closes: Vec<Decimal> = candles[start..=end].iter().map(|c| c.close()).collect();
Self::er_from_closes(&closes)
}
pub fn compute_from_closes(&self, closes: &[Decimal]) -> Result<Decimal, IndicatorError> {
let min_required = self.lookback + 1;
if closes.len() < min_required {
return Err(IndicatorError::InsufficientData {
required: min_required,
actual: closes.len(),
});
}
let end = closes.len() - 1;
let start = end - self.lookback;
Self::er_from_closes(&closes[start..=end])
}
fn er_from_closes(closes: &[Decimal]) -> Result<Decimal, IndicatorError> {
let net = (closes[closes.len() - 1] - closes[0]).abs();
let mut path = Decimal::ZERO;
for j in 1..closes.len() {
path += (closes[j] - closes[j - 1]).abs();
}
if path.is_zero() {
return Ok(Decimal::ZERO);
}
Ok(net / path)
}
fn er_at(candles: &[Candle], start: usize, end: usize) -> Result<Decimal, IndicatorError> {
let window = &candles[start..=end];
let net = (window[window.len() - 1].close() - window[0].close()).abs();
let mut path = Decimal::ZERO;
for j in 1..window.len() {
path += (window[j].close() - window[j - 1].close()).abs();
}
if path.is_zero() {
return Ok(Decimal::ZERO);
}
Ok(net / path)
}
#[must_use]
pub fn lookback(&self) -> usize {
self.lookback
}
}
#[cfg(test)]
#[path = "efficiency_ratio_tests.rs"]
mod tests;
impl Indicator for EfficiencyRatio {
fn name(&self) -> &str {
&self.name
}
fn warmup_period(&self) -> usize {
self.lookback + 1
}
fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
let min_required = self.lookback + 1;
if candles.len() < min_required {
return Err(IndicatorError::InsufficientData {
required: min_required,
actual: candles.len(),
});
}
let mut values = Vec::with_capacity(candles.len() - self.lookback);
for i in self.lookback..candles.len() {
let start = i - self.lookback;
let er = Self::er_at(candles, start, i)?;
values.push((candles[i].timestamp(), er));
}
Ok(Series::new(values))
}
}