Calculate the Average True Range (ATR) Easy with Pandas DataFrames

What will we cover in this tutorial?

In this tutorial we will cover the following

Step 1: Read historic stock prices from Yahoo! Finance API

To read data from Yahoo! Finance API we use Pandas-Datareader, which has a direct method.

This requires that we give a start date on how old data we want to retrieve.

import pandas_datareader as pdr
import datetime as dt

start = dt.datetime(2020, 1, 1)
data = pdr.get_data_yahoo("NFLX", start)


This we result in similar output.

	High	Low	Open	Close	Volume	Adj Close
2021-02-12	561.250000	550.849976	556.940002	556.520020	2195900	556.520020
2021-02-16	563.630005	552.729980	557.289978	557.280029	2622400	557.280029
2021-02-17	555.250000	543.030029	550.989990	551.340027	2069600	551.340027
2021-02-18	550.000000	538.229980	549.000000	548.219971	2456200	548.219971
2021-02-19	548.989990	538.809998	548.000000	540.219971	2838600	540.219971

Calculate the Average True Range (ATR)

The Average True Range (ATR) is calculated as follows, as defines it.

This can be calculated as follows.

import numpy as np
import pandas_datareader as pdr
import datetime as dt

start = dt.datetime(2020, 1, 1)
data = pdr.get_data_yahoo("NFLX", start)

high_low = data['High'] - data['Low']
high_close = np.abs(data['High'] - data['Close'].shift())
low_close = np.abs(data['Low'] - data['Close'].shift())

ranges = pd.concat([high_low, high_close, low_close], axis=1)
true_range = np.max(ranges, axis=1)

atr = true_range.rolling(14).sum()/14

Where we use the 14 days standard.

Visualize the ATR and the stock price

We will use Matplotlib to visualize it as it integrates well with DataFrames from Pandas.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
data['Close'].plot(ax=ax, secondary_y=True, alpha=0.3)

This will result in a chart similar to this one.

Want to learn more?

Check out this full FREE course learning about Risk and Return with Pandas DataFrames (Python).

17 Replies to “Calculate the Average True Range (ATR) Easy with Pandas DataFrames”

    1. Hi Ricky,
      Yes, it could have been done by using rolling mean.
      Notice I did it with sum instead. Just to follow the formula from



    1. Hi Geoff,
      Great point.
      It can be imported – but as we do not use any direct methods from pandas, it is enough to use it from the pandas_datareader.
      Cheers, Rune

    1. Hi Bahaa,
      Great to connect with you.
      Not sure what you are lookin for?
      This example used Pandas-datareader to get the data returned in a DataFrame (Pandas) to make the calculation.

  1. Why does the ATR calculation results in different values than the ATR calculation in Tradingview or other Platforms on the the same instrument with the same lookback period?

    1. Hi Mark,
      Great question.
      I have just checked the calculations – they are corresponding to the ones on Maybe they are not the same they use on platforms you compare with.
      Cheers, Rune

  2. The final ATR is incorrect. When you are pulling the max in the true_range dataframe, that is when the issue occurs. Because you have used the shift() in the other series, the max, just takes the high_low series and it is assigned as max for that row – this causes the whole rolling.sum()/14 part to pull incorrect values, resulting in an incorrect final ATR.

    1. Let’s see if we can figure it out.

      We have:

      TR = max( (H – L), | H – C_p | , | L – C_p | )

      n: 14
      H (current high): data[‘High’]
      L (current low): data[‘Low’]
      C_p (Previous close): data[‘Close’].shift()

      Notice that the shift ( data[‘Close’].shift() ) is used to get the previous close. And yes, that means the first value will be NaN.

      That should make:

      TR = max( data[‘High’] – data[‘Low’], abs( data[‘High’] – data[‘Close’].shift() ), abs( data[‘Low’] – data[‘Close’].shift() ) )

      That can be converted to:

      high_low = data[‘High’] – data[‘Low’]
      high_close = abs(data[‘High’] – data[‘Close’].shift())
      low_close = abs(data[‘Low’] – data[‘Close’].shift())

      ranges = concat([high_low, high_close, low_close], axis=1)
      TR = max(ranges, axis=1)

      I am not sure where the mistake should be?

      Don’t get me wrong – I am curious too?

  3. P.S.: I have tried using truncate but with no success
    If you have a solution to this, I would really appreciate it. Thank you.

  4. Thank you Rune!

    Getting up to speed w/ Python myself and was actually coding a strategy for trading when I found your code. Cool stuff that saved me time and I learned few tricks in manipulating dataframes.
    I find your results inline with mine from an excel spreadsheet and TOS. Only exception could be the 1st TrueRange whose calculation is limited to “High-Low”, lacking the previous Close value from the dataset.

    If compared to Tradestation or any platform with full dataset, starting before datetime(2020, 1, 1), you’ll get a different result on this one.
    Same on the 1st ATR which does include this 1st TR 🙂

    1. Hi Jean-Marc,

      Thanks for the feedback.

      The 1st row of data is incomplete, as it does not have previous close. Also the first 14 values of ATR are not correct, as the data to calculate them are not present. To deal with that you can read more data and discard them, as you also say.

      If you have more insights in other differences with the calculations of ATR and tranding platforms, please let me know.

      Cheers, Rune

  5. Yep, that’s why people are getting differing results from trading platforms. Also, several platforms may not use the ‘standard’ 14-day window. It may be a 21-day, 50-day, etc. The use of exponential weighted moving average versus simple moving average versus weighted with alpha correction will also result in somewhat different numbers. In the end, I’m not sure that all matters much except that it’s worth being aware when the numbers don’t align with another source. In the end, it’s only ‘context’ for trading and nothing hard and fast, anyway. ATR is a great tool to get some idea of position sizing for expected losses (stop losses) as it’s a volatility metric.

    Also, my apologies for the earlier mistake regarding the Pandas DataReader. I was up tinkering around at 1 AM on some night I should have been in bed and used my own code with my own data source ( and then used a direct CSV import. In my exhaustion I had forgotten that I had altered portions of your code and then corrected you for my own error in translation.

Leave a Reply