Skip to main content

quant_indicators/
atr.rs

1//! Average True Range (ATR) indicator.
2
3use std::collections::VecDeque;
4
5use quant_primitives::Candle;
6use rust_decimal::Decimal;
7
8use crate::error::IndicatorError;
9use crate::indicator::Indicator;
10use crate::series::Series;
11
12/// Average True Range indicator.
13///
14/// Measures market volatility by decomposing the entire range of an asset
15/// price for a given period. ATR uses the greatest of:
16/// - Current high minus current low
17/// - Absolute value of current high minus previous close
18/// - Absolute value of current low minus previous close
19///
20/// # Formula
21///
22/// True Range = max(high - low, |high - prev_close|, |low - prev_close|)
23/// ATR = Smoothed average of True Range over period
24///
25/// # Example
26///
27/// ```
28/// use quant_indicators::{Indicator, Atr};
29/// use quant_primitives::Candle;
30/// use chrono::Utc;
31/// use rust_decimal_macros::dec;
32///
33/// let ts = Utc::now();
34/// let candles: Vec<Candle> = (0..20).map(|i| {
35///     let d = rust_decimal::Decimal::from(i);
36///     Candle::new(dec!(100) + d, dec!(110) + d, dec!(90) + d, dec!(100) + d, dec!(1000), ts).unwrap()
37/// }).collect();
38/// let atr = Atr::new(14).unwrap();
39/// let series = atr.compute(&candles).unwrap();
40/// ```
41#[derive(Debug, Clone)]
42pub struct Atr {
43    period: usize,
44    name: String,
45}
46
47impl Atr {
48    /// Create a new ATR indicator with the specified period.
49    ///
50    /// Standard period is 14.
51    ///
52    /// # Errors
53    ///
54    /// Returns `InvalidParameter` if period is 0.
55    pub fn new(period: usize) -> Result<Self, IndicatorError> {
56        if period == 0 {
57            return Err(IndicatorError::InvalidParameter {
58                message: "ATR period must be > 0".to_string(),
59            });
60        }
61        Ok(Self {
62            period,
63            name: format!("ATR({})", period),
64        })
65    }
66}
67
68impl Indicator for Atr {
69    fn name(&self) -> &str {
70        &self.name
71    }
72
73    fn warmup_period(&self) -> usize {
74        // Need period + 1 candles (period true ranges require period + 1 prices)
75        self.period + 1
76    }
77
78    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
79        let required = self.period + 1;
80        if candles.len() < required {
81            return Err(IndicatorError::InsufficientData {
82                required,
83                actual: candles.len(),
84            });
85        }
86
87        // Calculate true ranges
88        let mut true_ranges = Vec::with_capacity(candles.len() - 1);
89
90        // First TR is just high - low (no previous close)
91        true_ranges.push(candles[0].high() - candles[0].low());
92
93        // Subsequent TRs use the full formula
94        for i in 1..candles.len() {
95            let tr = true_range(&candles[i], candles[i - 1].close());
96            true_ranges.push(tr);
97        }
98
99        let mut values = Vec::with_capacity(candles.len() - required + 1);
100        let period_dec = Decimal::from(self.period as u64);
101
102        // First ATR is simple average of first `period` true ranges
103        let initial_sum: Decimal = true_ranges[..self.period].iter().sum();
104        let mut atr = initial_sum / period_dec;
105        // Timestamp from last candle in the initial window
106        let ts = candles[self.period - 1].timestamp();
107        values.push((ts, atr));
108
109        // Subsequent ATRs use smoothed average
110        for (i, tr) in true_ranges.iter().enumerate().skip(self.period) {
111            // Smoothed: (prev_ATR * (period - 1) + current_TR) / period
112            atr = (atr * (period_dec - Decimal::ONE) + *tr) / period_dec;
113            // true_ranges[i] corresponds to candles[i]
114            let ts = candles[i].timestamp();
115            values.push((ts, atr));
116        }
117
118        Ok(Series::new(values))
119    }
120}
121
122/// Calculate True Range for a candle given previous close.
123///
124/// True Range = max(high - low, |high - prev_close|, |low - prev_close|)
125pub fn true_range(candle: &Candle, prev_close: Decimal) -> Decimal {
126    let high_low = candle.high() - candle.low();
127    let high_prev = (candle.high() - prev_close).abs();
128    let low_prev = (candle.low() - prev_close).abs();
129
130    high_low.max(high_prev).max(low_prev)
131}
132
133/// Compute the simple mean of true-range values in a sliding window.
134///
135/// Pure math used by rolling ATR adapters: push `new_tr` into the window,
136/// evict the oldest if the window exceeds `period`, then return the mean.
137///
138/// Returns [`Decimal::ZERO`] only when the window is empty (should not happen
139/// in normal use).
140pub fn rolling_atr_mean(window: &mut VecDeque<Decimal>, new_tr: Decimal, period: usize) -> Decimal {
141    window.push_back(new_tr);
142    if window.len() > period {
143        window.pop_front();
144    }
145    let count = Decimal::from(window.len());
146    if count > Decimal::ZERO {
147        let sum: Decimal = window.iter().copied().sum();
148        sum / count
149    } else {
150        Decimal::ZERO
151    }
152}
153
154#[cfg(test)]
155#[path = "atr_tests.rs"]
156mod tests;