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;