Do Stocks Exhibit Momentum? A reality check [2021]
![Do Stocks Exhibit Momentum? A reality check [2021]](https://firemymoneymanager.com/wp-content/uploads/2021/01/fmsq.png)
In this article we will look at whether stocks exhibit momentum. That is, can a stock’s movement direction in the previous period help predict the stock’s movement in the current period?
As an initial example, let’s take a look at the S&P ETF, SPY. Between January 2010 and August 2020 there were 49 months where SPY had a positive return and 18 when it did not. So the probability that it would go up in a given month is 73%.
Now, let’s see if there is a higher probability that a SPY will go up given that the previous month it went up. If we look at P(current month is up|previous month is up) the percentage is almost exactly the same: 73%.
So it doesn’t appear that the SPY exhibits momentum on a monthly scale, but do other stocks?
Related articles
Is momentum real?
Let’s first take a step back and ask ourselves: is momentum a real thing for stocks? If you pick only stocks that have a certain directional trend (we won’t look at magnitude in this article–just direction), will you be correct?
We start by taking a look at a simple case. If a stock has been up (or down) for the past 2 months, is it more likely to be up this month?
To determine the answer, we will adapt our code from our finding alpha article to add this:
mv = pd.DataFrame(columns = ['ticker', 'total_months', 'months_reverse', 'months_cont', ]) def same_direction(p,q): if p > 0 and q > 0: return True elif p < 0 and q < 0: return True else: return False # start at 6 to skip date and fama factors for i in range(6, len(table.columns)): r = table[table.columns[i]] total = reversals = continuations = 0 max_steps = 3 for k in range(max_steps, len(r)): same_dir = True for j in range(2, max_steps): if not same_direction(r[k-1], r[k-j]): same_dir = False if same_dir: total += 1 if same_direction(r[k-1],r[k]): continuations += 1 elif r[k] != 0: reversals += 1 mv.loc[i-6] = [ table.columns[i], total, reversals, continuations, ]
This will give us a table of all stocks, and for each one, we will count the total number of times we see 2 months of the same direction, the number of times we see that followed by a third month of the same direction (a continuation), and the number of times we see it followed by a third month of the opposite direction (a reversal).
Running the code and filtering by stocks where are more continuations than reversals, we get this:
ticker total_months months_reverse months_cont
4 AAON 30 14 16
5 AAP 34 16 18
6 AAPL 37 14 23
9 AAXN 38 18 20
10 AB 30 10 20
... ... ... ...
2650 YUM 34 16 18
2652 ZBH 29 14 15
2655 ZG 34 13 21
2659 ZIXI 35 15 20
2660 ZNGA 34 16 18
[1139 rows x 4 columns]
If we filter by stocks with more reversals, we get a similar number. This happens with other lengths of look-back as well. It appears unlikely that on the whole stocks exhibit momentum on a monthly scale.
Do individual stocks exhibit momentum?
To determine if individual stocks exhibit momentum, we can start by loading all of our stocks and counting how many months see a continuation (current month has positive/negative returns and previous month had positive/negative returns) vs how many months see a reversal (current month is up while previous month was down and vice-versa).
We modify the code above for individual stocks:
mv = pd.DataFrame(columns = ['ticker', 'total_months', 'months_reverse', 'months_cont', 'up_up', 'down_up', 'up_down', 'down_down', 'oddsratio', 'pval']) # start at 6 to skip date and fama factors for i in range(6, len(table.columns)): r = table[table.columns[i]] up_up = 0 up_down = 0 down_up = 0 down_down = 0 for k in range(1, len(r)): if r[k-1] > 0: if r[k] > 0: up_up = up_up + 1 elif r[k] > 0: up_down = up_down + 1 elif r[k - 1] < 0: if r[k] > 0: down_up = down_up + 1 elif r[k] > 0: down_down = down_down + 1 tbl = [[up_up, up_down], [down_up, down_down]] fe = stats.fisher_exact(tbl) mv.loc[i-6] = [ table.columns[i], up_up + down_down + down_up + up_down, down_up + up_down, up_up + down_down, up_up, #prev=up,curr=up down_up, #prev=down/nc,curr=up up_down, #prev=up,curr=down/nc down_down, #prev=down/nc,curr=down/nc fe[0], fe[1] ]
This gives us a table of tickers and counts, like this one:
ticker total_months months_reverse months_cont
3618 URG 66 45 21
2483 NTN 66 44 22
3410 TDE 66 44 22
3657 UZA 66 44 22
3802 WEYS 66 44 22
... ... ... ...
503 CAS 66 12 54
1539 GSY 66 12 54
1856 IVAN 66 9 57
2526 NYC 66 6 60
1355 FST 66 4 62
[3937 rows x 4 columns]
Now, we can perform some statistics magic on this data. Let’s add a proportion and confidence interval:
mv['p'] = mv['months_cont']/mv['total_months'] mv['lb'] = mv['p'] - 2 * np.sqrt((mv['p'] * (1-mv['p']))/mv['total_months']) mv['ub'] = mv['p'] + 2 * np.sqrt((mv['p'] * (1-mv['p']))/mv['total_months'])
Now, we can find all tickers where the lower bound of the ~95% confidence interval is greater than .5:
mv.loc[mv['lb']>.5]
And here is our result:
ticker lb
177 AOA 0.501792
3288 SPY 0.501792
3282 SPWR 0.501792
1281 FIX 0.501792
3239 SOXS 0.501792
... ...
1300 FLTR 0.617632
2859 PWZ 0.617632
2223 MINT 0.617632
1355 FST 0.689109
1539 GSY 0.723230
[186 rows x 2 columns]
So we have around 200 tickers out of our initial dataset of several thousand that show a statistically significant momentum. There is also a small number of tickers that show a statistically significant likelihood of monthly reversal every month.
Also, it’s worth noting that most of these tickers are bond or other ETFs.
Do stocks exhibit momentum on a daily basis?
So we found a small number of stocks and tickers that exhibit momentum on a monthly basis. What if we looked for momentum on a daily basis? Is it possible to find more evidence of momentum?
Let’s change the code a bit and sample on a daily basis. Again, we use most of the code in the finding alpha article, combined with the code above. We restrict this code to stocks only, in order to avoid getting bond or other ETFs.
ticker lb
1737 NWE 0.500076
2425 TWI 0.500204
1980 QDEL 0.500584
1045 GNMK 0.500591
1758 OCN 0.501020
1779 OLN 0.501156
1043 GMLP 0.501563
2069 RPT 0.501908
980 FUN 0.502247
188 ARR 0.502382
1474 MAIN 0.502526
1 AAL 0.502562
1167 HSC 0.503028
1275 IRM 0.503185
838 ET 0.503230
2407 TRTN 0.503478
2346 TGI 0.503570
1772 OHI 0.504286
508 CLMT 0.504529
1155 HOV 0.504783
2071 RRD 0.504903
320 BIP 0.505364
403 CAP 0.506590
2322 TCP 0.507743
1706 NSH 0.507784
1823 PAA 0.508233
207 ATCO 0.509246
1350 KKR 0.509387
1687 NNBR 0.510040
683 DCP 0.510314
1800 ORBC 0.510368
2038 RIG 0.510560
1745 NXST 0.511846
1007 GEL 0.520256
382 BX 0.523700
So it looks like there are a few stocks that actually exhibit momentum on a daily basis, and their lower bounds are only very slightly above 50%.
Sorting portfolios by momentum
Let’s try a different approach: most researchers, when performing analyses like this, create grouped portfolios to determine whether they can achieve outsized returns based on the variable in question.
In this section, we will try to group stocks by momentum and see if we find any statistically significant differences in the cross section of returns. We define our momentum variable as the total percentage return of a stock over the past 12 months. This gives a number which can be compared over all stocks, allowing us to perform a sort.
Let’s change the code a bit to divide the previous 12 months into quintiles.
mv = pd.DataFrame(columns = ['date', 'q1return', 'q2return', 'q3return', 'q4return', 'q5return']) for i in range(12, len(table.index)): d = table.loc[table.index[i]] d1 = table.loc[table.index[i-1]] d2 = table.loc[table.index[i-2]] d12 = table.loc[table.index[i-12]] m12return = (d2-d12)/d12 curreturn = (d-d1)/d1 r = np.array_split(m12return.sort_values(), 5) mv.loc[i-12] = [ table.index[i], curreturn.loc[r[0].index].mean(), curreturn.loc[r[1].index].mean(), curreturn.loc[r[2].index].mean(), curreturn.loc[r[3].index].mean(), curreturn.loc[r[4].index].mean(), ]
Now, we can run this code and get average returns. Let’s start by testing it on old data (1960s to 2000) to see if we see the expected momentum:
q1return 0.008605
q2return 0.012372
q3return 0.018795
q4return 0.014145
q5return 0.016277
There’s a reasonably strong correlation between quantile and returns. In fact, if you invested in Q5 stocks for the duration of the period, you would see almost a 1% per month greater return than you would have with Q1 stocks.
However, that seems to have changed. Let’s run the numbers from 2010 to today:
q1return 0.013495
q2return 0.011013
q3return 0.009778
q4return 0.009727
q5return 0.012298
That shows a better return with Q1 stocks than Q5. If you picked stocks by momentum, you would actually do slightly worse off than not.
What if we filter out certain stocks?
Prior literature shows that there was momentum for the market as a whole, as well as specifically for most groups of stocks. However, we have found that’s no longer the case. The question remains: is there a certain subset of stocks that do exhibit momentum?
If we change our filter to only give us top 50 market cap stocks, we get these results:
q1return 0.003904
q2return 0.008318
q3return 0.009299
q4return 0.012628
q5return 0.015866
However, these may be affected by outliers, so let’s drop the top and bottom percentile. We still see reasonably strong momentum:
q1return 0.004868
q2return 0.009519
q3return 0.008828
q4return 0.013255
q5return 0.013416
Let’s look at the smallest stocks and their returns:
q1return 0.302399
q2return -0.006149
q3return 0.005664
q4return -0.003950
q5return -0.001966
And here are the returns for micro cap stocks:
q1return 0.031270
q2return 0.030559
q3return 0.004018
q4return 0.004052
q5return 0.007646
So it definitely appears that there is an exploitable momentum component to returns for the largest stocks.
Do the prior month’s returns tell us anything?
It turns out that the prior month’s returns do tell us something. Let’s run the code above, and change the 12 month return to just look at the previous month’s returns. Here’s what we get:
q1return 0.016625
q2return 0.011384
q3return 0.011978
q4return 0.008397
q5return 0.007921
If we try different time periods, the results are equally pronounced. There is nearly a 1% average difference in monthly returns between the Q1 stocks and the Q5 stocks. This holds for many different timescales as well.
As soon as you try a longer lookback horizon beyond one month, the differences disappear. Here, we compared this month to the two prior months:
q1return 0.014810
q2return 0.011204
q3return 0.012287
q4return 0.011534
q5return 0.010064
Conclusion
Unfortunately, the strong momentum relations of the past mostly seem to be gone. However, there are some things we can correlate with past movement. For instance, there does appear to be a statistically significant negative correlation between the previous month’s returns and this month’s returns. And for the largest large-cap stocks, there does appear to be an exploitable momentum component to returns.
The question is: can we actually use momentum to beat a benchmark? We will look at that in our next article.
1 Comment
However, momentum and value are often seen as nemeses, with one usually doing badly if the other is on the upswing. After all, if a value stock climbs for long enough, it is no longer cheap enough to qualify as one. Yet the superlative performance of many long-shunned areas that dominate the value stock universe has fed expectations that they will also be categorised as momentum stocks when weightings get periodically reshuffled.