What will we cover in this tutorial?
In this tutorial we will cover the following
- Read historic time series data from Yahoo! Finance using Pandas-Datareader.
- Define what the Average True Range (ATR) is.
- Calculate the Average True Range (ATR).
- Visualize it on a chart using matplotlib.
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)
print(data.tail())
This we result in similar output.
High Low Open Close Volume Adj Close
Date
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
Step 2: Calculate the Average True Range (ATR)
The Average True Range (ATR) is calculated as follows, as investopedia.org 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.
Step 3: 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()
atr.plot(ax=ax)
data['Close'].plot(ax=ax, secondary_y=True, alpha=0.3)
plt.show()
This will result in a chart similar to this one.

12% Investment Solution
Would you like to get 12% in return of your investments?
D. A. Carter promises and shows how his simple investment strategy will deliver that in the book The 12% Solution. The book shows how to test this statement by using backtesting.
Did Carter find a strategy that will consistently beat the market?
Actually, it is not that hard to use Python to validate his calculations. But we can do better than that. If you want to work smarter than traditional investors then continue to read here.
Python for Finance: Unlock Financial Freedom and Build Your Dream Life
Discover the key to financial freedom and secure your dream life with Python for Finance!
Say goodbye to financial anxiety and embrace a future filled with confidence and success. If you’re tired of struggling to pay bills and longing for a life of leisure, it’s time to take action.
Imagine breaking free from that dead-end job and opening doors to endless opportunities. With Python for Finance, you can acquire the invaluable skill of financial analysis that will revolutionize your life.
Make informed investment decisions, unlock the secrets of business financial performance, and maximize your money like never before. Gain the knowledge sought after by companies worldwide and become an indispensable asset in today’s competitive market.
Don’t let your dreams slip away. Master Python for Finance and pave your way to a profitable and fulfilling career. Start building the future you deserve today!
Python for Finance a 21 hours course that teaches investing with Python.
Learn pandas, NumPy, Matplotlib for Financial Analysis & learn how to Automate Value Investing.
“Excellent course for anyone trying to learn coding and investing.” – Lorenzo B.

Hello Rune…why are you still dividing the value for the ATR by 14 when you already have the rolling mean of 14
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 Investopedia.org.
true_range.rolling(14).sum()/14
Cheers,
Rune
Not bad, but you’re missing one line of code: import pandas as pd
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
I need a set of codes from the Panda Library
Necessary in the fastest time with its data.
Thank you
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.
Cheers,
Rune
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?
Hi Mark,
Great question.
I have just checked the calculations – they are corresponding to the ones on Investopedia.org. Maybe they are not the same they use on platforms you compare with.
Cheers, Rune
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.
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?
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.
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 🙂
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
There are multiple different common ways to calculate ATR. They may be a simple moving average, a weighted moving average, or even a Wilder’s calculation. For a quick examination, you can look at this source https://www.macroption.com/atr-excel/
Thank you for sharing. I will be looking into it. Cheers.
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 (Tiingo.com) 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.
Hi Geoff,
Thank you for sharing such a detailed answer.
Cheers,
Rune
Hi Rune, and thank so much for your fantastic work. I have a question, all it’s ok for simple stock but when I want to use the same strategy for a multiindex dataframe the final dataframe its only one column. Please I apreciate if you could help me to solve that.
Hi Hugo, just be sure how the DataFrame is structured. This would be the case when you get a DataFrame from something like this:
data = pdr.get_data_yahoo([“NFLX”, “AAPL”, “TWTR”], start)
?
We should have a column for each stock but the output only give one:
[*********************100%***********************] 3 of 3 completed
Date
2020-01-02 NaN
2020-01-03 NaN
2020-01-06 NaN
2020-01-07 NaN
2020-01-08 NaN
…
2022-01-05 13.772858
2022-01-06 14.377860
2022-01-07 14.642147
2022-01-10 14.552861
2022-01-11 14.535714
Length: 512, dtype: float64
This should do the trick:
import numpy as np
import pandas_datareader as pdr
import pandas as pd
import datetime as dt
start = dt.datetime(2020, 1, 1)
tickers = ["NFLX", "AAPL"]
data = pdr.get_data_yahoo(tickers, 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 = pd.DataFrame()
for ticker in tickers:
true_range[ticker] = np.max(ranges[ticker], axis=1)
atr = true_range.rolling(14).sum() / 14
print(atr)
Perfect!, Thank you.
Thank you Rune!
I do not understand the following sentences.
ranges = pd.concat([high_low, high_close, low_close], axis=1)
it returns us a df with three corresponding columns each one those values. The next sentence,
true_range = np.max(ranges, axis=1)
That makes?. Select the column with larger values? If so, isn’t that column always going to be the one corresponding to df[‘High’] – df[‘Low’]? I will be grateful if you explain these sentences to us.
Thanks in advance.
Hi Efueyo,
Let’s look at the formula for for TR
TR = max[ (H – L), |H – Cp|, |L – Cp| ]
Where H: Current High, L: Current Low, Cp: Previous Close.
Therefore you don’t have that H – L is always max.
Hope it makes sense.
Rune
I think the key is to understand that, in that column, we will have, in each row, the maximum of the values in the three columns of that row. high_low, high_close, low_close
The formula in previous answer is for the variables you mention.
Thanks Rune. I understand yet.
Hi Rune, excellent post to solve this function but you only calculates the “first ATR” and it is not exactly we need, it is only valid the first time. When we have the first ATR value the upcoming ATR values should be calculated like shows investopedia in this paragraph:
https://www.investopedia.com/terms/a/atr.asp#toc-the-average-true-range-atr-formula
So we need to calculate “first ATR” in the 14th dataframe row and then calculate the real ATR, if you have a lot historical data downloaded the real ATR calculated should be exactly the same as any ATR indicator in any platform. At least 250 historical data ir required for this.
I’m new in python now so I don’t know how we can add this code to your function. I need to study more.
Thank you.