Performance of Moving Average as Price Supports

Moving averages (abbreviated as MA usually) are venerable tools in the field of stock price technical analysis. The diagram that possibly made it widely known is found in W. I. King’s Elements of Statistical Method (1912):

The moving average creates a compression of past data prices and provides a view of the past periods' trend behavior. This first example is for a temperature data series, setting a trend for novel statistical methods it was quickly adapted (or conscripted) into the analysis and prediction of prices in the stock market. The moving average, in its various forms, filters out what could be noise in price values and is claimed to provide levels of support or resistance for prices in various time frames. If this is the case we can analyze multiple moving averages for different stocks and check if there is a supporting or resisting effect in place that makes the price rebound when it touches these limits. We will start with one example, taken totally at random, Gamestop (GME), and see if we can trick some algorithm to send some traffic our way.


Using Quantconnect´s research environment we can set up a historical price call for GME, loading first an instance of the Quantbook and adding the symbol to it:

self = QuantBook()
ticker = 'GME'
symbol = self.AddEquity(ticker).Symbol

We will use data from 2010 to 2015 for our analysis as the price behavior of GME could be subjected to changes in time and recent trading action does not necessarily reflect past behavior:

start = datetime(2010, 1, 1)
end = datetime(2015, 1, 1)
history = self.History(symbol, start, end, Resolution.Daily)

The shape of the dataframe returned by the history call is this, with the open, closing, high and low prices, and traded volume for the period:


The dataframe is prepared to house multiple ticker symbols in the "level 0" of its index, we are just going to use the daily closing price and we do not need this level 0 index now:

prices = history['close'].unstack(level=0) 

The transformation results in this other dataframe, more useful to us:


Now we can loop over a list of time windows to obtain a set of simple moving average series. We can make it as long as we want, lets use first typical values:

windows = [5, 20, 30, 50, 100, 200]
averages = prices.copy()
for win in windows:
    averages['MA_' + str(win)] = prices.rolling(window=win).mean()
averages.dropna(inplace=True)

Our averages dataframe now contains a set of all rolling window means for the periods specified in the "windows" list:

We can obtain the typical moving averages plot:

cmap = 'Spectral'
averages.plot(figsize=(16,10), grid=True, colormap=cmap);

There is a lot of information in this plot and it is difficult to reach a conclusion regarding the relationship of price to the moving average. It is clear that the slowest moving averages, 100-day and 200-day, are not contacted very often. When these long-term averages are contacted a "cross-over" happens and, in general, establishes a long term trend. To quantify the times the moving averages touch the prices, irrespective of where they are crossing from, we will locate the column with the price in price_col and find the difference between this price and the given moving average, when the sign of the difference changes across two consecutive observation the moving average has been crossed:

price_col = prices.columns[0]
for win in windows:
    averages['diff_EMA_'+str(win)] = averages[price_col] - averages['MA_' + str(win)]
    shift = averages['diff_EMA_'+str(win)].shift(1)
    diff = averages['diff_EMA_'+str(win)]
    averages['cross_'+str(win)] = np.sign(shift)!=np.sign(diff)

We can also find now which of the moving averages results in more price crossings, raw, not looking at whether this price crossing resulted in a rebound against the mean, or breached it:

cross_columns = [col for col in averages.columns if 'cross' in col]
rate = {}
for column in cross_columns:
    rate[column] = averages[column].value_counts(normalize=True)[True]

The results tell us, probably as expected, that faster averages result in more price crossings, as the "noise" in the price is less filtered. Also, for the 50-day and 100-day averages, in this time frame, the proportion of crossings is very similar. Take into account that the 100-day moving average is constructed with twice the amount of information used to construct the 50-day average; there may be some information loss or gain in one of them:

{'cross_5': 0.279508970727101,
 'cross_20': 0.11237016052880075,
 'cross_30': 0.0708215297450425,
 'cross_50': 0.06326723323890462,
 'cross_100': 0.06515580736543909,
 'cross_200': 0.04815864022662889}

We will use the 50 day MA to set up our modeling hypothesis. We will check whether this specific moving average acts as a support or resistance. If this is the case, for an arbitrary future (keeping it symmetric, 50 days into the future), any touch of the MA50 line will result in the price not falling anymore. The complete averages dataframe can be sliced for the MA50 data only, the first observation has to be dropped as it generates a false cross point:

ema_50 = averages[[price_col, 'MA_50', 'diff_EMA_50', 'cross_50']]
ema_50.iloc[1:][[price_col, 'MA_50']].plot(figsize=(16, 12));

We can visualize this case:


Now, if this particular moving average is acting as support (or resistance) any touch, or most touches, by the price line should result in this price rebounding:

ema_50['50_fut'] = ema_50[price_col].shift(-50)
ema_50[ema_50['cross_50'] == True].dropna()
ema_50['cross_type'] = ema_50['diff_EMA_50'] > 0
ema_50[ema_50.cross_50 == True].dropna()
ema_50['Fut_Price_direction'] = ema_50['50_fut'] > ema_50[price_col]
ema_50['Hit'] = ema_50['cross_type'] != ema_50['Fut_Price_direction']

We can check now how many times that the price breached the moving average the future price (50 days on, for simplicity) went against the change. The percentage of 'hits' or correct predictions is slightly above 50%:

True     0.53
False    0.47

53% of the times the price collided with the moving average at 50 periods the price rebounds when looking at the price 50 days into the future. We can, of course, extend this model for all the moving averages, or exponential moving averages or volume-weighted moving averages... or any average whatsoever and check if there is a historical directionality prediction power on it. In future installments we will try and develop a trading strategy using this procedure, we will use GME, or maybe we will have to use silver (in the form of SLV ETF).


Information in ostirion.net does not constitute financial advice; we do not hold positions in any of the companies or assets that we mention in our posts at the time of posting. If you require quantitative model development, deployment, verification, or validation do not hesitate and contact us. We will be also glad to help you with your machine learning or artificial intelligence challenges when applied to asset management, trading or, risk evaluations. The model described in this publication will be published in the future, please register by signing up.

23 views0 comments

Recent Posts

See All