quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Average True Range (ATR) indicator.

use std::collections::VecDeque;

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

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

/// Average True Range indicator.
///
/// Measures market volatility by decomposing the entire range of an asset
/// price for a given period. ATR uses the greatest of:
/// - Current high minus current low
/// - Absolute value of current high minus previous close
/// - Absolute value of current low minus previous close
///
/// # Formula
///
/// True Range = max(high - low, |high - prev_close|, |low - prev_close|)
/// ATR = Smoothed average of True Range over period
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, Atr};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..20).map(|i| {
///     let d = rust_decimal::Decimal::from(i);
///     Candle::new(dec!(100) + d, dec!(110) + d, dec!(90) + d, dec!(100) + d, dec!(1000), ts).unwrap()
/// }).collect();
/// let atr = Atr::new(14).unwrap();
/// let series = atr.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Atr {
    period: usize,
    name: String,
}

impl Atr {
    /// Create a new ATR indicator with the specified period.
    ///
    /// Standard period is 14.
    ///
    /// # Errors
    ///
    /// Returns `InvalidParameter` if period is 0.
    pub fn new(period: usize) -> Result<Self, IndicatorError> {
        if period == 0 {
            return Err(IndicatorError::InvalidParameter {
                message: "ATR period must be > 0".to_string(),
            });
        }
        Ok(Self {
            period,
            name: format!("ATR({})", period),
        })
    }
}

impl Indicator for Atr {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        // Need period + 1 candles (period true ranges require period + 1 prices)
        self.period + 1
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let required = self.period + 1;
        if candles.len() < required {
            return Err(IndicatorError::InsufficientData {
                required,
                actual: candles.len(),
            });
        }

        // Calculate true ranges
        let mut true_ranges = Vec::with_capacity(candles.len() - 1);

        // First TR is just high - low (no previous close)
        true_ranges.push(candles[0].high() - candles[0].low());

        // Subsequent TRs use the full formula
        for i in 1..candles.len() {
            let tr = true_range(&candles[i], candles[i - 1].close());
            true_ranges.push(tr);
        }

        let mut values = Vec::with_capacity(candles.len() - required + 1);
        let period_dec = Decimal::from(self.period as u64);

        // First ATR is simple average of first `period` true ranges
        let initial_sum: Decimal = true_ranges[..self.period].iter().sum();
        let mut atr = initial_sum / period_dec;
        // Timestamp from last candle in the initial window
        let ts = candles[self.period - 1].timestamp();
        values.push((ts, atr));

        // Subsequent ATRs use smoothed average
        for (i, tr) in true_ranges.iter().enumerate().skip(self.period) {
            // Smoothed: (prev_ATR * (period - 1) + current_TR) / period
            atr = (atr * (period_dec - Decimal::ONE) + *tr) / period_dec;
            // true_ranges[i] corresponds to candles[i]
            let ts = candles[i].timestamp();
            values.push((ts, atr));
        }

        Ok(Series::new(values))
    }
}

/// Calculate True Range for a candle given previous close.
///
/// True Range = max(high - low, |high - prev_close|, |low - prev_close|)
pub fn true_range(candle: &Candle, prev_close: Decimal) -> Decimal {
    let high_low = candle.high() - candle.low();
    let high_prev = (candle.high() - prev_close).abs();
    let low_prev = (candle.low() - prev_close).abs();

    high_low.max(high_prev).max(low_prev)
}

/// Compute the simple mean of true-range values in a sliding window.
///
/// Pure math used by rolling ATR adapters: push `new_tr` into the window,
/// evict the oldest if the window exceeds `period`, then return the mean.
///
/// Returns [`Decimal::ZERO`] only when the window is empty (should not happen
/// in normal use).
pub fn rolling_atr_mean(window: &mut VecDeque<Decimal>, new_tr: Decimal, period: usize) -> Decimal {
    window.push_back(new_tr);
    if window.len() > period {
        window.pop_front();
    }
    let count = Decimal::from(window.len());
    if count > Decimal::ZERO {
        let sum: Decimal = window.iter().copied().sum();
        sum / count
    } else {
        Decimal::ZERO
    }
}

#[cfg(test)]
#[path = "atr_tests.rs"]
mod tests;