Algorithmic Trading in Python: Simple Moving Averages | by Aidan Wilson | Towards Data Science

A moving average is one of the most basic technical indicators used to analyze stocks. “Moving average” is a broad term and there are many variations used by analysts to smooth out price data and analyze trends.
Moving averages will require a time period for calculations. For example, an investor may choose a 50-day moving average, where the past 50 days in the data will be used to calculate the average. Smaller windows of time are more sensitive to changes in the price data due to the fewer number of data points and larger time periods are less sensitive to daily changes.
Moving averages may be used by traders as their main strategy or as a part of their trading strategy. The most popular and simple way to use moving averages is to use a cross over strategy which, if followed, will hopefully tell a trader when to buy and sell. This strategy will be discussed further.
In this example we will be using the SPDR S&P 500 Trust exchange traded fund (ETF), commonly known for its ticker symbol SPY. SPY is the largest, and one of the most popular exchange traded funds (ETF) in the world and mimics the performance of the S&P 500 index.
The S&P 500 index, or S&P for short, measures the performance of 500 of the largest companies listed on the stock exchanges in the United States. When you buy a share of SPY, you buy a small percentage of each of the 500 companies in the S&P 500.
We’ll start with the four import statements. We will use Matplotlib to visualize the price data and the analysis. Numpy is an extremely popular and useful library that provides high level functions for efficient calculations. Pandas is the go-to library for data analysis and manipulation and our data will be stored in Pandas DataFrames.
Our data will be pulled from a library called yfinance. I wrote a yfinance tutorial recently that may be worth reading before this (found here), but is not required and everything you need to know will be covered here.
import yfinance as yfimport pandas as pdimport numpy as npimport matplotlib.pyplot as plt
Using yfinance, we will pull historic price data from SPY.
spy_ticker = yf.Ticker('SPY')spy = yf.download('SPY')
Next, we will do some quick data visualization to get an idea of what we are working with. We can use Matplotlib to create two plots: one showing the historic stock price data and one showing the volume over time. Nothing too fancy, just getting a feel for things.
fig, ax = plt.subplots(nrows=1, ncols=2, sharex=True, figsize=(15,5))ax[0].set_title('SPY Close', fontsize=15)ax[0].plot(spy['Close'])ax[1].set_title('SPY Volume', fontsize=15)ax[1].plot(spy['Volume'])
Bullish crossover occurs when the short-term SMA crosses above the long-term SMA.
Bearish crossover occurs when the short-term SMA crosses below the long-term SMA.
Imagine we have two SMA lines: a 50 day (short-term) and a 100 day (long-term). The moving averages will be calculated and plotted over the price data. When the 50 day moving average crosses above the 100 day moving average, this would be a buy signal. If the 50 day were to then cross below the 100 day, it would be a sell signal. The hope of this is to buy low and sell high.
Pn = The price of an asset at period n
n = The number of total periods
To calculate the various simple moving averages, we will use two functions from Pandas: .rolling() and .mean().
.rolling() will take care of the moving window calculations. It takes the window size (e.g. 10, 20, etc) and performs calculations on only the data points within that window.
.mean() will calculate the mean, or average, across the data points within the window provided in the .rolling() function. For example, if the window size is set to 10 data points, the average of those 10 data points will be calculated.
The figure below demonstrates how the rolling mean is calculated over data points. In this example, there are ten price data points (P) with a window size of just two data points for simplicity. The green shaded region contains the two points that will be used to calculate the average and it “rolls” across all ten data points while calculating the averages for each pair.
We will calculate various window sizes to later test which combinations work best. Here, we will calculate windows of sizes 10, 20, 50 and 100.
SMA10 = spy['Close'].rolling(window = 10).mean()SMA20 = spy['Close'].rolling(window = 20).mean()SMA50 = spy['Close'].rolling(window = 50).mean()SMA100 = spy['Close'].rolling(window = 100).mean()
Now we will use Matplotlib to plot the original stock data along with each of the moving average lines we just calculated. To clearly see the lines and where some of them may cross over, we will plot just the last 300 data points, or the most recent 300 trading day’s closing prices.
Take note where a few of these moving average lines cross. Do any of the lines that cross over indicate the price will rise or drop?
# taking last 300 trading daysplt.figure(figsize=(15, 6))plt.plot(spy['Close'][-300:], label='SPY')plt.plot(SMA10[-300:], label='SMA10')plt.plot(SMA20[-300:], label='SMA20')plt.plot(SMA50[-300:], label='SMA50')plt.plot(SMA100[-300:], label='SMA100')plt.legend(loc='upper left', fontsize=15)
Looking at some of the moving average lines, you may notice that they can be used as an indicator of the direction the price is moving. For example, notice how the shorter-term SMA lines cross over the longer-term SMA lines starting at around 2020–03. This is an indication to sell before the price drops even more and, sure enough, the price does significantly decline after each bearish crossover.
def get_points_above(sma_low, sma_high):    points_above = {}    for pair in zip(sma_low, sma_high):        if pair[0] >= pair[1]:            date = sma_low[sma_low == pair[0]].index[0]            points_above[date] = pair[0]                points_above = pd.Series(points_above, name='Price_Points')    points_above.index.name = 'Date'                return points_abovepoints_above_SMA50 = get_points_above(SMA20, SMA50)
We can write the above function to find the data points where the shorter-term line is above the longer-term line.
SMA20 = SMA20.reset_index()SMA50 = SMA50.reset_index()
We can then reset the index for each data frame so that the dates become a new column. This is just to make things easier.
Then we will create a new data frame names “crossovers”. This data frame will contain the points where the shorter-term SMA line crosses above or below the longer-term SMA line. This will be key later when we plot the crossover points, and these points are crucial for the trading strategy.
crossovers = pd.DataFrame()crossovers['Dates'] = SMA20['Date']crossovers['Price'] = [i for i in spy['Close']]crossovers['SMA20'] = SMA20['Close']crossovers['SMA50'] = SMA50['Close']crossovers['position'] = crossovers['SMA20'] >= crossovers['SMA50']crossovers['pre-position'] = crossovers['position'].shift(1)crossovers['Crossover'] = np.where(crossovers['position'] == crossovers['pre-position'], False, True)crossovers['Crossover'][0] = Falseprint(crossovers)
We can then change the data frame so that we have signals for when to buy and when to sell. We will create a “Signal” feature and a “Binary Signal” feature. The “Signal” column is more for us, where it tells us in English what exactly the signal is. The binary signal will be either 0 or 1, where 1 means to buy and 0 means to sell.
crossovers = crossovers.loc[crossovers['Crossover'] == True]crossovers = crossovers.reset_index()crossovers = crossovers.drop(['position', 'pre-position', 'Crossover', 'index'], axis=1)crossovers['Signal'] = np.nancrossovers['Binary_Signal'] = 0.0for i in range(len(crossovers['SMA20'])):    if crossovers['SMA20'][i] > crossovers['SMA50'][i]:        crossovers['Binary_Signal'][i] = 1.0        crossovers['Signal'][i] = 'Buy'    else:        crossovers['Signal'][i] = 'Sell'print(crossovers)
Now all we have to do is just plot our results and see if everything worked how it was supposed to. To make things easier to view at first, lets just plot the last 600 trading days in the data set. This will allow us to clearly see the crossovers and the signals.
# taking last 600 trading daysSMA20 = spy['Close'].rolling(window=20).mean()SMA50 = spy['Close'].rolling(window=50).mean()plt.figure(figsize=(17, 8))plt.plot(spy['Close'][-600:], label='SPY')plt.plot(SMA20[-600:], label='SMA20')plt.plot(SMA50[-600:], label='SMA50')plt.plot(crossovers.loc[crossovers.Signal == 'Buy']['Dates'][-5:],          crossovers['SMA20'][crossovers.Signal == 'Buy'][-5:],        '^', markersize=15, color='g', label='Buy')plt.plot(crossovers.loc[crossovers.Signal == 'Sell']['Dates'][-4:],          crossovers['SMA20'][crossovers.Signal == 'Sell'][-4:],        'v', markersize=15, color='r', label='Sell')plt.legend(loc='upper left', fontsize=15)
After just doing a quick visual analysis, the strategy seems like it would do relatively well! This strategy would tell us to buy at the green up arrow signals and to sell at the red down arrow signals. We can see that the strategy would typically “buy low and sell high”, which we are all told is the goal of investing.
And below will be the same plot, but configured for the last 1,000 trading days in the data.
Thank you for reading and I hope you enjoyed and maybe even learned a thing or two!
Note from Towards Data Science’s editors: While we allow independent authors to publish articles in accordance with our rules and guidelines, we do not endorse each author’s contribution. You should not rely on an author’s works without seeking professional advice. See our Reader Terms for details.