Skip to main content

nautilus_indicators/volatility/
atr.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::fmt::{Debug, Display};
17
18use nautilus_model::data::{Bar, QuoteTick, TradeTick};
19
20use crate::{
21    average::{MovingAverageFactory, MovingAverageType},
22    indicator::{Indicator, MovingAverage},
23};
24
25/// An indicator which calculates an Average True Range (ATR) across a rolling window.
26#[repr(C)]
27#[derive(Debug)]
28#[cfg_attr(
29    feature = "python",
30    pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
31)]
32#[cfg_attr(
33    feature = "python",
34    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
35)]
36pub struct AverageTrueRange {
37    pub period: usize,
38    pub ma_type: MovingAverageType,
39    pub use_previous: bool,
40    pub value_floor: f64,
41    pub value: f64,
42    pub count: usize,
43    pub initialized: bool,
44    ma: Box<dyn MovingAverage + Send + 'static>,
45    has_inputs: bool,
46    previous_close: f64,
47}
48
49impl Display for AverageTrueRange {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(
52            f,
53            "{}({},{},{},{})",
54            self.name(),
55            self.period,
56            self.ma_type,
57            self.use_previous,
58            self.value_floor,
59        )
60    }
61}
62
63impl Indicator for AverageTrueRange {
64    fn name(&self) -> String {
65        stringify!(AverageTrueRange).to_string()
66    }
67
68    fn has_inputs(&self) -> bool {
69        self.has_inputs
70    }
71
72    fn initialized(&self) -> bool {
73        self.initialized
74    }
75
76    fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
77        Ok(())
78    }
79
80    fn handle_trade(&mut self, _trade: &TradeTick) {}
81
82    fn handle_bar(&mut self, bar: &Bar) {
83        self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
84    }
85
86    fn reset(&mut self) {
87        self.ma.reset();
88        self.previous_close = 0.0;
89        self.value = 0.0;
90        self.count = 0;
91        self.has_inputs = false;
92        self.initialized = false;
93    }
94}
95
96impl AverageTrueRange {
97    /// Creates a new [`AverageTrueRange`] instance.
98    #[must_use]
99    pub fn new(
100        period: usize,
101        ma_type: Option<MovingAverageType>,
102        use_previous: Option<bool>,
103        value_floor: Option<f64>,
104    ) -> Self {
105        Self {
106            period,
107            ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
108            use_previous: use_previous.unwrap_or(true),
109            value_floor: value_floor.unwrap_or(0.0),
110            value: 0.0,
111            count: 0,
112            previous_close: 0.0,
113            ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
114            has_inputs: false,
115            initialized: false,
116        }
117    }
118
119    pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
120        if self.use_previous {
121            if !self.has_inputs {
122                self.previous_close = close;
123            }
124            self.ma.update_raw(
125                f64::max(self.previous_close, high) - f64::min(low, self.previous_close),
126            );
127            self.previous_close = close;
128        } else {
129            self.ma.update_raw(high - low);
130        }
131
132        self.apply_floor();
133        self.increment_count();
134    }
135
136    fn apply_floor(&mut self) {
137        if self.value_floor == 0.0 || self.value_floor < self.ma.value() {
138            self.value = self.ma.value();
139        } else {
140            // Floor the value
141            self.value = self.value_floor;
142        }
143    }
144
145    const fn increment_count(&mut self) {
146        self.count += 1;
147
148        if !self.initialized {
149            self.has_inputs = true;
150
151            if self.count >= self.period {
152                self.initialized = true;
153            }
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use rstest::rstest;
161
162    use super::*;
163    use crate::testing::assert_approx_equal;
164
165    #[rstest]
166    fn test_name_returns_expected_string() {
167        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
168        assert_eq!(atr.name(), "AverageTrueRange");
169    }
170
171    #[rstest]
172    fn test_str_repr_returns_expected_string() {
173        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), Some(true), Some(0.0));
174        assert_eq!(format!("{atr}"), "AverageTrueRange(10,SIMPLE,true,0)");
175    }
176
177    #[rstest]
178    fn test_period() {
179        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
180        assert_eq!(atr.period, 10);
181    }
182
183    #[rstest]
184    #[case(None, "SimpleMovingAverage")]
185    #[case(Some(MovingAverageType::Simple), "SimpleMovingAverage")]
186    #[case(Some(MovingAverageType::Exponential), "ExponentialMovingAverage")]
187    #[case(
188        Some(MovingAverageType::DoubleExponential),
189        "DoubleExponentialMovingAverage"
190    )]
191    #[case(Some(MovingAverageType::Wilder), "WilderMovingAverage")]
192    #[case(Some(MovingAverageType::Hull), "HullMovingAverage")]
193    fn test_ma_type_creates_expected_inner_ma(
194        #[case] ma_type: Option<MovingAverageType>,
195        #[case] expected: &str,
196    ) {
197        let atr = AverageTrueRange::new(10, ma_type, None, None);
198        assert_eq!(atr.ma.name(), expected);
199    }
200
201    #[rstest]
202    fn test_initialized_without_inputs_returns_false() {
203        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
204        assert!(!atr.initialized());
205    }
206
207    #[rstest]
208    fn test_initialized_with_required_inputs_returns_true() {
209        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
210        for _ in 0..10 {
211            atr.update_raw(1.0, 1.0, 1.0);
212        }
213        assert!(atr.initialized());
214    }
215
216    #[rstest]
217    fn test_value_with_no_inputs_returns_zero() {
218        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
219        assert_eq!(atr.value, 0.0);
220    }
221
222    #[rstest]
223    fn test_value_with_epsilon_input() {
224        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
225        let epsilon = f64::EPSILON;
226        atr.update_raw(epsilon, epsilon, epsilon);
227        assert_eq!(atr.value, 0.0);
228    }
229
230    #[rstest]
231    fn test_value_with_one_ones_input() {
232        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
233        atr.update_raw(1.0, 1.0, 1.0);
234        assert_eq!(atr.value, 0.0);
235    }
236
237    #[rstest]
238    fn test_value_with_one_input() {
239        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
240        atr.update_raw(1.00020, 1.0, 1.00010);
241        assert_approx_equal(atr.value, 0.0002);
242    }
243
244    #[rstest]
245    fn test_value_with_three_inputs() {
246        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
247        atr.update_raw(1.00020, 1.0, 1.00010);
248        atr.update_raw(1.00020, 1.0, 1.00010);
249        atr.update_raw(1.00020, 1.0, 1.00010);
250        assert_approx_equal(atr.value, 0.0002);
251    }
252
253    #[rstest]
254    fn test_value_with_close_on_high() {
255        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
256        let mut high = 1.00010;
257        let mut low = 1.0;
258
259        for _ in 0..1000 {
260            high += 0.00010;
261            low += 0.00010;
262            let close = high;
263            atr.update_raw(high, low, close);
264        }
265        assert_approx_equal(atr.value, 0.0001);
266    }
267
268    #[rstest]
269    fn test_value_with_close_on_low() {
270        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
271        let mut high = 1.00010;
272        let mut low = 1.0;
273
274        for _ in 0..1000 {
275            high -= 0.00010;
276            low -= 0.00010;
277            let close = low;
278            atr.update_raw(high, low, close);
279        }
280        assert_approx_equal(atr.value, 0.0001);
281    }
282
283    #[rstest]
284    fn test_floor_with_ten_ones_inputs() {
285        let floor = 0.00005;
286        let mut floored_atr =
287            AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, Some(floor));
288
289        for _ in 0..20 {
290            floored_atr.update_raw(1.0, 1.0, 1.0);
291        }
292        assert_eq!(floored_atr.value, 5e-05);
293    }
294
295    #[rstest]
296    fn test_floor_with_exponentially_decreasing_high_inputs() {
297        let floor = 0.00005;
298        let mut floored_atr =
299            AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, Some(floor));
300        let mut high = 1.00020;
301        let low = 1.0;
302        let close = 1.0;
303
304        for _ in 0..20 {
305            high -= (high - low) / 2.0;
306            floored_atr.update_raw(high, low, close);
307        }
308        assert_eq!(floored_atr.value, floor);
309    }
310
311    #[rstest]
312    fn test_reset_successfully_returns_indicator_to_fresh_state() {
313        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
314        for _ in 0..1000 {
315            atr.update_raw(1.00010, 1.0, 1.00005);
316        }
317        atr.reset();
318        assert!(!atr.initialized);
319        assert_eq!(atr.value, 0.0);
320    }
321
322    #[rstest]
323    fn test_reset_resets_inner_ma() {
324        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
325        atr.update_raw(1.00010, 1.0, 1.00005);
326        atr.reset();
327        assert_eq!(atr.ma.count(), 0);
328    }
329}