quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Kaufman Efficiency Ratio (ER) indicator.
//!
//! Measures how efficiently price moves in one direction versus total movement.
//!
//! # Formula
//!
//! ```text
//! ER(N) = |close[i] - close[i-N]| / sum(|close[j] - close[j-1]| for j in (i-N+1)..=i)
//! ```
//!
//! - ER → 1.0: price moved efficiently in one direction (trending)
//! - ER → 0.0: lots of movement, no directional progress (choppy)
//!
//! # Reference
//!
//! Kaufman, P.J. (1995) "Smarter Trading"

use quant_primitives::Candle;
use rust_decimal::Decimal;

use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;

/// Kaufman Efficiency Ratio indicator.
#[derive(Debug, Clone)]
pub struct EfficiencyRatio {
    lookback: usize,
    name: String,
}

impl EfficiencyRatio {
    /// Create a new `EfficiencyRatio` with the specified lookback period.
    ///
    /// # Errors
    ///
    /// Returns `InvalidParameter` if lookback is 0.
    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),
        })
    }

    /// Compute the ER from the last `lookback` candles.
    ///
    /// Returns a single Decimal in \[0, 1\].
    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)
    }

    /// Compute ER from a slice of close prices.
    ///
    /// Requires at least `lookback + 1` close prices (same as `compute_ratio`).
    /// Useful when only close prices are available (e.g., ring buffer).
    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])
    }

    /// Core ER computation from a contiguous slice of close prices.
    ///
    /// Expects exactly `lookback + 1` values (first is the anchor, rest are the window).
    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)
    }

    /// Compute ER for a specific window: candles[start..=end].
    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)
    }

    /// The lookback period.
    #[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))
    }
}