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_core::correctness::FAILED;
19use nautilus_model::data::{Bar, QuoteTick, TradeTick};
20
21use crate::{
22    average::{MovingAverageFactory, MovingAverageType},
23    indicator::{Indicator, MovingAverage},
24    support::MAX_PERIOD,
25};
26
27/// An indicator which calculates an Average True Range (ATR) across a rolling window.
28#[repr(C)]
29#[derive(Debug)]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
33)]
34#[cfg_attr(
35    feature = "python",
36    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
37)]
38pub struct AverageTrueRange {
39    pub period: usize,
40    pub ma_type: MovingAverageType,
41    pub use_previous: bool,
42    pub value_floor: f64,
43    pub value: f64,
44    pub count: usize,
45    pub initialized: bool,
46    ma: Box<dyn MovingAverage + Send + 'static>,
47    has_inputs: bool,
48    previous_close: f64,
49}
50
51impl Display for AverageTrueRange {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(
54            f,
55            "{}({},{},{},{})",
56            self.name(),
57            self.period,
58            self.ma_type,
59            self.use_previous,
60            self.value_floor,
61        )
62    }
63}
64
65impl Indicator for AverageTrueRange {
66    fn name(&self) -> String {
67        stringify!(AverageTrueRange).to_string()
68    }
69
70    fn has_inputs(&self) -> bool {
71        self.has_inputs
72    }
73
74    fn initialized(&self) -> bool {
75        self.initialized
76    }
77
78    fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
79        Ok(())
80    }
81
82    fn handle_trade(&mut self, _trade: &TradeTick) {}
83
84    fn handle_bar(&mut self, bar: &Bar) {
85        self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
86    }
87
88    fn reset(&mut self) {
89        self.ma.reset();
90        self.previous_close = 0.0;
91        self.value = 0.0;
92        self.count = 0;
93        self.has_inputs = false;
94        self.initialized = false;
95    }
96}
97
98impl AverageTrueRange {
99    /// Creates a new [`AverageTrueRange`] instance.
100    ///
101    /// # Panics
102    ///
103    /// Panics if `period` is outside `1..=MAX_PERIOD`, or `value_floor` is negative or non-finite.
104    #[must_use]
105    pub fn new(
106        period: usize,
107        ma_type: Option<MovingAverageType>,
108        use_previous: Option<bool>,
109        value_floor: Option<f64>,
110    ) -> Self {
111        Self::new_checked(period, ma_type, use_previous, value_floor).expect(FAILED)
112    }
113
114    pub(crate) fn new_checked(
115        period: usize,
116        ma_type: Option<MovingAverageType>,
117        use_previous: Option<bool>,
118        value_floor: Option<f64>,
119    ) -> anyhow::Result<Self> {
120        anyhow::ensure!(
121            (1..=MAX_PERIOD).contains(&period),
122            "period must be in 1..={MAX_PERIOD}"
123        );
124        let value_floor = value_floor.unwrap_or(0.0);
125        anyhow::ensure!(
126            value_floor.is_finite() && value_floor >= 0.0,
127            "value_floor must be finite and non-negative"
128        );
129        Ok(Self {
130            period,
131            ma_type: ma_type.unwrap_or(MovingAverageType::Wilder),
132            use_previous: use_previous.unwrap_or(true),
133            value_floor,
134            value: 0.0,
135            count: 0,
136            previous_close: 0.0,
137            ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Wilder), period),
138            has_inputs: false,
139            initialized: false,
140        })
141    }
142
143    pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
144        if !high.is_finite()
145            || !low.is_finite()
146            || !close.is_finite()
147            || high < low
148            || close < low
149            || close > high
150        {
151            return;
152        }
153        let range = if self.use_previous && self.has_inputs {
154            f64::max(self.previous_close, high) - f64::min(low, self.previous_close)
155        } else {
156            high - low
157        };
158
159        self.ma.update_raw(range);
160
161        if self.use_previous {
162            self.previous_close = close;
163        }
164
165        self.increment_count();
166
167        if self.initialized {
168            self.apply_floor();
169        }
170    }
171
172    fn apply_floor(&mut self) {
173        if self.value_floor == 0.0 || self.value_floor < self.ma.value() {
174            self.value = self.ma.value();
175        } else {
176            // Floor the value
177            self.value = self.value_floor;
178        }
179    }
180
181    fn increment_count(&mut self) {
182        self.count += 1;
183
184        if !self.initialized {
185            self.has_inputs = true;
186
187            if self.ma.initialized() {
188                self.initialized = true;
189            }
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use rstest::rstest;
197
198    use super::*;
199    use crate::{
200        stubs::{stub_quote, stub_trade},
201        testing::assert_approx_equal,
202    };
203
204    #[rstest]
205    fn test_name_returns_expected_string() {
206        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
207        assert_eq!(atr.name(), "AverageTrueRange");
208    }
209
210    #[rstest]
211    fn test_str_repr_returns_expected_string() {
212        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), Some(true), Some(0.0));
213        assert_eq!(format!("{atr}"), "AverageTrueRange(10,SIMPLE,true,0)");
214    }
215
216    #[rstest]
217    fn test_period() {
218        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
219        assert_eq!(atr.period, 10);
220    }
221
222    #[rstest]
223    #[case(None, "WilderMovingAverage")]
224    #[case(Some(MovingAverageType::Simple), "SimpleMovingAverage")]
225    #[case(Some(MovingAverageType::Exponential), "ExponentialMovingAverage")]
226    #[case(
227        Some(MovingAverageType::DoubleExponential),
228        "DoubleExponentialMovingAverage"
229    )]
230    #[case(Some(MovingAverageType::Wilder), "WilderMovingAverage")]
231    #[case(Some(MovingAverageType::Hull), "HullMovingAverage")]
232    fn test_ma_type_creates_expected_inner_ma(
233        #[case] ma_type: Option<MovingAverageType>,
234        #[case] expected: &str,
235    ) {
236        let atr = AverageTrueRange::new(10, ma_type, None, None);
237        assert_eq!(atr.ma.name(), expected);
238    }
239
240    #[rstest]
241    fn test_initialized_without_inputs_returns_false() {
242        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
243        assert!(!atr.initialized());
244    }
245
246    #[rstest]
247    fn test_initialized_with_required_inputs_returns_true() {
248        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
249        for _ in 0..10 {
250            atr.update_raw(1.0, 1.0, 1.0);
251        }
252        assert!(atr.initialized());
253    }
254
255    #[rstest]
256    fn test_value_with_no_inputs_returns_zero() {
257        let atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
258        assert_eq!(atr.value, 0.0);
259    }
260
261    #[rstest]
262    fn test_value_with_epsilon_input() {
263        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
264        let epsilon = f64::EPSILON;
265        atr.update_raw(epsilon, epsilon, epsilon);
266        assert_eq!(atr.value, 0.0);
267    }
268
269    #[rstest]
270    fn test_value_with_one_ones_input() {
271        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
272        atr.update_raw(1.0, 1.0, 1.0);
273        assert_eq!(atr.value, 0.0);
274    }
275
276    #[rstest]
277    fn test_value_with_one_input() {
278        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
279        atr.update_raw(1.00020, 1.0, 1.00010);
280        assert_eq!(atr.value, 0.0);
281        assert!(!atr.initialized);
282    }
283
284    #[rstest]
285    fn test_value_with_three_inputs() {
286        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
287        atr.update_raw(1.00020, 1.0, 1.00010);
288        atr.update_raw(1.00020, 1.0, 1.00010);
289        atr.update_raw(1.00020, 1.0, 1.00010);
290        assert_eq!(atr.value, 0.0);
291        assert!(!atr.initialized);
292    }
293
294    #[rstest]
295    fn test_value_with_close_on_high() {
296        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
297        let mut high = 1.00010;
298        let mut low = 1.0;
299
300        for _ in 0..1000 {
301            high += 0.00010;
302            low += 0.00010;
303            let close = high;
304            atr.update_raw(high, low, close);
305        }
306        assert_approx_equal(atr.value, 0.0001);
307    }
308
309    #[rstest]
310    fn test_value_with_close_on_low() {
311        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
312        let mut high = 1.00010;
313        let mut low = 1.0;
314
315        for _ in 0..1000 {
316            high -= 0.00010;
317            low -= 0.00010;
318            let close = low;
319            atr.update_raw(high, low, close);
320        }
321        assert_approx_equal(atr.value, 0.0001);
322    }
323
324    #[rstest]
325    fn test_floor_with_ten_ones_inputs() {
326        let floor = 0.00005;
327        let mut floored_atr =
328            AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, Some(floor));
329
330        for _ in 0..20 {
331            floored_atr.update_raw(1.0, 1.0, 1.0);
332        }
333        assert_eq!(floored_atr.value, 5e-05);
334    }
335
336    #[rstest]
337    #[case(MovingAverageType::Simple, 3)]
338    #[case(MovingAverageType::Exponential, 3)]
339    #[case(MovingAverageType::DoubleExponential, 5)]
340    #[case(MovingAverageType::Wilder, 3)]
341    #[case(MovingAverageType::Hull, 3)]
342    fn test_selected_smoother_controls_readiness_and_floor(
343        #[case] ma_type: MovingAverageType,
344        #[case] first_ready: usize,
345    ) {
346        let mut indicator = AverageTrueRange::new(3, Some(ma_type), None, Some(3.0));
347        for count in 1..=10 {
348            indicator.update_raw(12.0, 10.0, 11.0);
349            assert_eq!(indicator.count, count);
350            assert_eq!(indicator.initialized, count >= first_ready);
351            assert_eq!(
352                indicator.value,
353                if count >= first_ready { 3.0 } else { 0.0 }
354            );
355        }
356    }
357
358    #[rstest]
359    fn test_rejected_candle_does_not_change_the_next_true_range() {
360        let mut indicator = AverageTrueRange::new(2, None, None, None);
361        indicator.update_raw(12.0, 10.0, 11.0);
362
363        for (high, low, close) in [
364            (10.0, 12.0, 11.0),
365            (12.0, 10.0, 13.0),
366            (12.0, 10.0, f64::NAN),
367        ] {
368            indicator.update_raw(high, low, close);
369            assert_eq!(indicator.count, 1);
370            assert_eq!(indicator.value, 0.0);
371            assert!(!indicator.initialized);
372        }
373        indicator.update_raw(22.0, 20.0, 21.0);
374        assert_eq!(indicator.count, 2);
375        assert_eq!(indicator.value, 6.5);
376        assert!(indicator.initialized);
377    }
378
379    #[rstest]
380    fn test_floor_with_exponentially_decreasing_high_inputs() {
381        let floor = 0.00005;
382        let mut floored_atr =
383            AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, Some(floor));
384        let mut high = 1.00020;
385        let low = 1.0;
386        let close = 1.0;
387
388        for _ in 0..20 {
389            high -= (high - low) / 2.0;
390            floored_atr.update_raw(high, low, close);
391        }
392        assert_eq!(floored_atr.value, floor);
393    }
394
395    #[rstest]
396    fn test_reset_successfully_returns_indicator_to_fresh_state() {
397        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
398        for _ in 0..1000 {
399            atr.update_raw(1.00010, 1.0, 1.00005);
400        }
401        atr.reset();
402        assert!(!atr.initialized);
403        assert_eq!(atr.value, 0.0);
404    }
405
406    #[rstest]
407    fn test_reset_resets_inner_ma() {
408        let mut atr = AverageTrueRange::new(10, Some(MovingAverageType::Simple), None, None);
409        atr.update_raw(1.00010, 1.0, 1.00005);
410        atr.reset();
411        assert_eq!(atr.ma.count(), 0);
412    }
413
414    #[rstest]
415    fn test_quote_and_trade_are_ignored(stub_quote: QuoteTick, stub_trade: TradeTick) {
416        let mut atr = AverageTrueRange::new(10, None, None, None);
417
418        let result = atr.handle_quote(&stub_quote);
419        atr.handle_trade(&stub_trade);
420
421        assert!(result.is_ok());
422        assert!(!atr.has_inputs());
423        assert_eq!(atr.count, 0);
424        assert_eq!(atr.value, 0.0);
425    }
426}