Exploring Moving Averages to Build Trend Following Strategies in Python | by Runar Aadnesen Østhaug | Towards Data Science

“Buy low, sell high” is a common goal everyone in finance wants to achieve. This however is more difficult than appears, since we don’t know where the top or bottom will be. We only know where we are right now, and where we have been. Many investors that sit and wait for the next recession in order to get a good entry point, miss out on the fantastic return in the bull market leading up, and the ones sitting in indices receive the benefits of the bull market but also the harsh reality of the bear market.
It is easier to follow the trend than to predict the top and bottom. So in this article, we will cover some basic methods of tracking market trend and try to achieve solid returns both during the bull markets and the bear markets.
The simple moving average (SMA) is a smoothing function that calculates the average of the past observations. It is a common technical indicator that is used to signal a price reversal.
If we have 1000 days of daily pricing data, the 100 day moving average is calculated by averaging the first 100 days and subsequently averaging 100 days at a time by shifting the range one day at a time.
Simple moving average based strategies revolve around using the relationship between the asset price and the moving average as a way of capturing the trend. We will use the point where the price and the MA cross over to determine our position.
We can either download the data from a website such as Yahoo finance and load it onto a DataFrame, or use an API to read it directly from an external server onto the Python memory.
import pandas as pddf = pd.read_csv("your_data.csv")
Quandl is an online database for financial data with a free and a premium account. The number of requests for non-users is limited, while users can get 300 calls every 10 seconds. More than enough for basic use.
We use our favorite stock of choice Apple, and download price data from the period 2009 through 2018.
myquandlkey = "myquandlkey"import quandlaapl = quandl.get("WIKI/AAPL", start_date="2009-01-01",                   end_date="2018-12-31", api_key=myquandlkey)
Another popular choice is the pandas_datareader, which is the one that will be used this time.
import pandas_datareader as webaapl = web.get_data_yahoo('AAPL',                           start=datetime.datetime(2009, 1, 1),                           end=datetime.datetime(2018, 12, 31))
After importing the data, let’s take a look at the DataFrame to check that everything looks correct
aapl.head()
For now, only keep the adjusted close, but the volume can be interesting to look at in further analysis. Remove the first row as we accidentally included New Year’s Eve 2008.
# New DataFrame only containing the Adj Close columnaapl = aapl[["Adj Close"]]# Rename the adjusted close column to Priceaapl.rename(columns={"Adj Close":"Price"}, inplace=True)# Remove the first row,aapl = aapl.iloc[1:]aapl.head()
Use the pandas rolling function for calculating the moving average, and add the result as a new column to the DataFrame.
aapl["100MA"] = aapl["Price"].rolling(window=100).mean()aapl
The 99 first moving average values show as NaN. This is because we needed the first 100 price observations to calculate the MA.
# import plotting packagesimport matplotlib.pyplot as pltimport matplotlib as mpl# Set color styleplt.style.use('seaborn-dark')plt.style.use("tableau-colorblind10")fig = plt.figure(figsize=(20,12))ax1 = plt.plot(aapl["Adj Close"])ax1 = plt.plot(aapl["100MA"])ax1 = plt.title("Apple daily ajusted close from 2009 through 2018", fontsize=22)ax1 = plt.xlabel("Date", fontsize=18)ax1 = plt.ylabel("Price", fontsize=18)ax1 = plt.legend(["Price", "100 day SMA"],prop={"size":20}, loc="upper left")plt.grid(True)plt.show()
The 100 day MA is a way of viewing the trend of the price by smoothing out the price. Since it is a lagging indicator it tells us what happened in the past, and although it is interesting to have a look it may not be used as a good strategy in and of itself. Let’s explore and see the outcome.
We will use two different strategies
Buy when the price exceeds the moving average, short sell when the price drops below the moving average.As an alternative we can replace the short position with cash position, making the strategy long only.
Buy when the price exceeds the moving average, short sell when the price drops below the moving average.
As an alternative we can replace the short position with cash position, making the strategy long only.
We could improve strategy 2 by holding low risk securities such as bonds instead of cash with just a small increase in volatility, but for simplicity, cash is king today.
We hope to catch the ride when the asset is trending upwards, gaining similar returns. When it trends downwards choose to sell out and wait, or short sell in order to benefit from the fall.
There can be several problems with a simple strategy like this. If the trend doesn’t manifest itself but gives a false positive, our strategy can end up buying or selling in the wrong direction and lose money. A lot of crossovers in a short time can pile up transaction costs. A solution to this can be to set a minimum distance between the price and the moving average before we trade, which can be explored further later.
Let’s create a new column containing “Long” or “Short” following strategy 1.
Position = []for i in range(0,apple.shape[0]):    if apple[“Adj Close”].iloc[i] > apple[“100MA”].iloc[i]:        Position.append(“Long”)    else:        Position.append(“Short”)apple[“Position”] = Positionapple.head()
Create a new column of price returns and add it to the DataFrame
apple[“return”] = (apple[“Adj Close”] — apple[“Adj Close”].shift())/apple[“Adj Close”].shift()# The first return is defined from the 2. row, so drop the firstapple.dropna(inplace=True)apple.head()
At day 1, set the value of each strategy equal to the price of Apple so it will be easy to compare how the value of the strategies moves compares to the price of Apple. Compare the price with the moving average and set a position “Long” or “Short”. The return at day 1 will be set using the percentage change from day 0. We can’t place a position yet because we only have the daily close, and need to wait until tomorrow.
At day 2 we go long or short based on the position from day 1, since we didn’t add or remove any value from day 1 to 2, the strategies will have the same value as day 1.
At day 3 we multiply the return, or change in Apple price from day 2 to 3, which is the return in row nr. 3, by each strategy.
edit: Keep in mind that the LongHold strategy is strategy 2 which is holding cash. A better name would be LongCash.
LongShort = [0]*apple.shape[0]LongHold = [0]*apple.shape[0]LongShort[0] = apple[“Adj Close”].iloc[0]LongShort[1] = apple[“Adj Close”].iloc[0]LongHold[0] = apple[“Adj Close”].iloc[0] LongHold[1] = apple[“Adj Close”].iloc[0]for i in range(0, apple.shape[0]-2):    if apple[“Position”].iloc[i] == “Long”:    LongShort[i+2] = LongShort[i+1]*(1+apple[“return”][i+2])    else:        LongShort[i+2] = LongShort[i+1]/(1+apple[“return”][i+2]) for i in range(0, apple.shape[0]-2):    if apple[“Position”].iloc[i] == “Long”:        LongHold[i+2] = LongHold[i+1]*(1+apple[“return”][i+2])    else:        LongHold[i+2] = LongHold[i+1]apple[“LongShort”] = LongShortapple[“LongHold”] = LongHoldapple.drop(apple.tail(1).index,inplace=True)apple.head()
Now plot the strategies together with the 100 day SMA
fig = plt.figure(figsize=(20,12))ax1 = plt.plot(apple[“Adj Close”])ax1 = plt.plot(apple[“100MA”])ax1 = plt.plot(apple[“LongShort”], color=”green”)ax1 = plt.plot(apple[“LongHold”], color=”brown”)ax1 = plt.title(“Apple daily ajusted close and strategy from 2009 through 2018”, fontsize=18)ax1 = plt.xlabel(“Date”, fontsize=18)ax1 = plt.ylabel(“Price”, fontsize=18)ax1 = plt.legend([“Price”, “100 day SMA”, “SMA crossover long/short strategy”, “SMA crossover long/cash strategy”],prop={“size”:22}, loc=”upper left”)plt.grid(True)plt.show()
In the last 10 years Apple experiences a very healthy growth. Because the SMA is lagging, the shorts ends up hurting us. A slight down-movement which causes a SMA crossover quickly turns into a sharp increase without the SMA graph catching up quick enough, leaving us shorting when the asset price is increasing. As can be seen, Long/Short strategy is crushed by the price process, while the Long/Hold strategy stays in the middle.
In 2012/2013 the shorting strategy works, increasing the value when Apple price is falling. The price is smooth, manifesting the trend easily. In 2015/2016 the strategy doesn’t work well. The price trend doesn’t manifest itself and exhibits high volatility.
Instead of the SMA, a more appropriate weighting function will give a higher vote to more recent observations. A popular version of this is the exponential moving average (EMA), which uses an exponentially decaying weighting. Since old observations have very little say, we may use the entire dataset as a lookback period to calculate the EMA.
The formula is given by
Implement the EMA with the same strategies as earlier. Simply compare the price with the EMA instead of the SMA.
At least for Apple, the EMA performs better then the SMA. This might be because it reacts quicker to sudden changes in the asset price. It still doesn’t overperform simply holding the asset.
Instead of using a single moving average, we can compare the relationship between two moving averages with different time periods. This is known as the moving average convergence divergence, or MACD.
The hypothesis is to buy or sell the asset depending on the trend captured by the MACD indicator. When the short term MA exceeds the long term, we go long, and when the long term MA exceeds the short term, we go short. In this analysis I have used the 21 day EMA and the 126 day EMA, representing one and six months of trading days. Since the backtests are run over a long time horizon I try to avoid using too short lookback periods. To write the code, calculate the 26 and 126 day EMA, and compare them instead of comparing the price with the MA.
Similarly to the EMA, we plot the and look at the results
This particular version of the MACD doesn’t perform well on this data. We will soon look at all the strategies on a variety of data sets and time frames too see how they have in different situations.
To generalize from the above analysis, I have used functions to make it easier to select which data we want to analyze, and what models we want to include.
I have also incorporating a summary statistics script to see how each of the strategies are performing in terms of max drawdown, sharpe, volatility, and annualized return, VaR, etc.
Now write a main function that will use the above functions, as well as a function that plots the result.
Let’s run the main function strategy() and the plot function plot_strategy(), as well as displaying the table of summary statistics. We calculate and plot the 3 MA variations and both the Long/Short and the Long/Hold for all.
Apple from 2009 through 2018, as done earlier
S&P 500 in the same period
start = datetime.date(2009, 1, 1)end = datetime.date(2018, 12, 31)ticker = “AAPL”days = 100df, table, df_return_of_strategy, df_positions, df_price_of_strategy = strategy(ticker, start, end, days=days, MA=True, EMA=True, MACD=True, LongHold=True, LongShort=True)plot_strategy(ticker=ticker, start=str(start), end=str(end), df=df_price_of_strategy)table
The moving average strategies doesn’t do well in the bull market. Shorting when the market is going up and still produce excess return is hard. The maximum drawdown and volatility is lowest among the cash portfolios as we can expect, which actually gives a higher Sharpe ratio in Apple than holding the stock in the period. Let’s look at two market crashes, leading up and during the fall.
S&P 500 from 2006 through 2010, “the global financial crisis”
NASDAQ Composite from 1998 through 2001, “Tech bubble”
Nikkei 225 during the 1990’s, “the lost decade”
There is no surprise that shorting during a bear market produces good results, but we initiated the positions a good while before the top, which doesn’t harm return too bad during the lead up. In the downfall however, the short positions benefits massively. One drawdown of this strategy is that we can’t detect sudden drops in price. In “black friday” 1987, the Dow Jones Industrial Average fell with 22.6% and the S&P 500 with over 18%. These strategies can’t protect us against such falls, and should be implemented in conjunction with other methods.
General motors from 2014 through 2019
Microsoft from 2002 through 2017
Sideways never manifest a trend, when the lagging MA catches up to the price, it is already too late. The trend is too short, which suggests a shorter MA could be a better choice. One thing to notice is that the EMA have higher returns in all but the last graph, and volatilities similar or lower. This version of the MACD doesn’t give very good results, although the most common version uses 12 days for the fast EMA, and 26 days for the slow.
The moving average crossover strategy for trend following is a well known simple approach for tracking the trend that in the period tested and with the data used, performs better in a bear market than a bull market. Although high volatile and stagnant markets can compromise its quality.
We have made several assumptions. There is no slippage or transaction fees. We only use adjusted daily close and can only make a transaction at that time. We don’t consider dividends or shorting restrictions.
We re-balance daily, which is not always feasible for some funds that have to move a large volume. They might be too late to the party. We also buy and sell simply based on the crossover, not considering a high oscillation between price and MA before they diverge.
For further analysis, it would be interesting to compare a variety of asset classes with contrasting behavior, for instance, high volatility vs. low volatility. It would be interesting to run through a set of assets each day, looking at the percentage distance between the price and the MA to see if trading those with a larger distance can reduce the number of false positives, and capture more solid trends better.
It would also be useful to perform more rigorous backtesting using an appropriate time series cross-validation approach. Stability in the model is very important. If we change a few parameters and the results changes drastically, we should scrap the model and start again. A backtest doesn’t prove anything, but using cross-validation and limit look-ahead bias can improve our confidence.
Disclaimer: If you find any mistakes please let me know. Also, note that this is just an exploration of some methods on some historic data. Past performance doesn’t guarantee future results.
Update (19/02/2020): this is a rewritten edition. I have revised a calculation error from the first version.
Github repository:
https://github.com/RunarOesthaug/MA-crossover_strategies
Thanks for reading!
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.