Do Stocks Exhibit Momentum? A reality check [2021]

Do Stocks Exhibit Momentum? A reality check [2021]

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

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.

Leave a Reply

Your email address will not be published.