Can you still beat the market by investing in low P/E ratio stocks? [2021]

Can you still beat the market by investing in low P/E ratio stocks? [2021]

In the 70s and 80s, there were many studies on U.S. stocks that showed that by investing in low P/E stocks (“value” stocks), it was possible to outperform the market. The most famous of those studies was by a researcher named Sanjoy Basu, who found that he could beat the market in both absolute and risk-adjusted terms by setting up a portfolio of low P/E stocks. But things have changed since the 70s, and with widespread algorithmic trading and so many more market participants, is it still possible to beat the market using low-P/E ratio value stocks?

References

Building a model

The first thing we will do is load in the data.

We use Quandl’s SF1 data, which includes PE ratios by quarter, along with prices. Unfortunately, the prices in the data are not adjusted for splits and other actions, so they’re essentially useless for our analysis. Instead, we will have to combine the SF1 daily data with daily adjusted prices to get a useable data table:

import pandas as pd  
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import quandl
import pickle
import csv
from datetime import date
import sys
import statsmodels.api as sm
import scipy.optimize as sco
import scipy.stats as stats

plt.style.use('fivethirtyeight')
np.random.seed(777)

pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 100)


alldata = pd.DataFrame()

pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 100)


try: 
    ticker_data = pd.read_csv("tickers.csv")
except FileNotFoundError as e:
    pass

ticker_data = ticker_data.loc[((ticker_data['table'] == 'SF1') | (ticker_data['table'] == 'SFPY')) & (ticker_data['isdelisted'] == 'N') & (ticker_data['currency'] == 'USD') & ((ticker_data.exchange == 'NYSE') | (ticker_data.exchange == 'NYSEMKT') | (ticker_data.exchange == 'NYSEARCA') | (ticker_data.exchange == 'NASDAQ'))]

for t in ticker_data['ticker']:
    if t == 'TRUE': continue
        
    print(t)
    
    try:
        data = pd.read_csv(t + ".csv", 
                            header = None,
                            usecols = [0, 1, 12],
                            names = ['ticker', 'date', 'adj_close'])
    
        if (len(data) < 2000): 
            continue
        
        alldata = alldata.append(data)
        
    except FileNotFoundError as e:
        pass


tickers = ticker_data['ticker']
df = alldata.set_index('date')


table = df.pivot(columns='ticker')
table.columns = [col[1] for col in table.columns]
table = table['2010-01-01':]

table.index = pd.to_datetime(table.index)
mtable = table.resample('BA').last()

dp = pd.DataFrame(columns = ['dp', 'change'])
mtable = mtable.pct_change()
mtable = mtable['2010-02-01':]

try: 
    daily = pd.read_csv("daily.csv")
except FileNotFoundError as e:
    pass

daily['date'] = pd.to_datetime(daily['date'])
daily = daily[daily['ticker'].isin(tickers)]
daily.set_index(['date','ticker'])

Choosing a strategy

In our previous article on P/E ratios, we found that there was a very weak relationship between individual stock P/E ratios and their returns. However, there are several studies (including ones with recent data) that suggest that you can achieve excess return by choosing stocks by P/E ratio.

So what can we do to reproduce those results? We will follow Fama and French’s methodology and try to segment the universe of stocks. Our plan: first segment all stocks by size, in order to minimize the confounding effect of size on returns. Then, we segment all stocks of a given size by P/E.

Quandl’s data segments stocks by size, from “Mega-cap” to “Nano-cap”. There are six different levels, which work perfectly for our analysis.

For each size segment, we run the following code to create a table of P/E ratios (“dp” in the code) and returns in the following period (this is definitely not the most efficient way of doing this, but it makes the code easier to read):

ind = mtable.index
for ticker in mtable.columns:
    print(ticker)
    t = mtable[ticker]
    for i in range(1, len(t)):
        cdp = []
        k=i-1
        while (len(cdp)< 1):
            cdp = daily.loc[(daily['ticker'] == ticker) & (daily['date'] == ind[k])]
            k -= 1
        dp.loc[len(dp)] = [float(cdp['pe']), t.iloc[i]]

Now, we can look at the returns for the different P/E segments. The P/E ratios go from lowest P/E to highest P/E:


Mega:

[0.13068503269408385,
 0.08326752061144241,
 0.1380462229254901,
 0.117719617051882,
 0.13216684174767918,
 0.14361117529086112,
 0.16333668810419613,
 0.205575724187894,
 0.259988838374162,
 0.23714170348735714]

Large:

[0.2602685291550176,
 0.1408437515613175,
 0.1472637837186117,
 0.12810054388845457,
 0.1439081333254362,
 0.13699477876245372,
 0.1345270411961278,
 0.1335653382380293,
 0.1457851810854936,
 0.15770516217138766]

Mid:

[0.16237083712643496,
 0.1440487438183479,
 0.12408102363908026,
 0.13220148708123755,
 0.09534966271276092,
 0.09051593111424015,
 0.07561844078734582,
 0.08486941622644986,
 0.11348194467952495,
 0.09144510435381892]

Small:

[0.19132591414519495,
 0.12820274038108942,
 0.09745781688023002,
 0.11474877987611032,
 0.09702471683984862,
 0.07064636319161406,
 0.09047229550531244,
 0.0828232086765549,
 0.04409273393810726,
 0.020270416006202725]

Micro:

[0.17124009248583802,
 0.14331515786674054,
 0.13093854405283298,
 0.1293375950116475,
 0.09443798212548235,
 0.08379384185333447,
 0.09268144187528211,
 0.048244706566666934,
 0.03798160669492658,
 0.06787391604198931]

Nano:

[0.09477948639891882,
 -0.2553948911601226,
 -0.06578412838435217,
 0.046386198100281335,
 0.21148023917830475,
 0.11741721425109572,
 -0.038402081750737435,
 0.16489474736888213,
 -0.00752587887449796,
 -0.04861228658528758]

Mega:

[0.19349508729629636,
 0.08170861789358486,
 0.10335788696558595,
 0.15172125575317202,
 0.12474620074587271,
 0.20554666421477968,
 0.14387144284321216,
 0.24155644548545854,
 0.21899822928155024,
 0.18844368661132702]

Large:

[0.18051089433141054,
 0.15161381644314398,
 0.1282648452872524,
 0.11635037171828862,
 0.14175540492858066,
 0.14988266567120198,
 0.14229371532314422,
 0.14706812855208015,
 0.13023004902981858,
 0.14152766956747506]

Mid:

[0.27945562084133735,
 0.17879796173281368,
 0.14067649967264556,
 0.12890605747331618,
 0.10475947497437907,
 0.13823494507685608,
 0.12353034935699093,
 0.11823404448760805,
 0.0729309362573854,
 0.14235647834799478]

Small:


Micro:


Nano:

[0.19349508729629636,
 0.08170861789358486,
 0.10335788696558595,
 0.15172125575317202,
 0.12474620074587271,
 0.20554666421477968,
 0.14387144284321216,
 0.24155644548545854,
 0.21899822928155024,
 0.18844368661132702]
Mega:

[0.009599684431421535,
 0.007604692930039574,
 0.010074241328499666,
 0.00906479345104507,
 0.012220214373535827,
 0.011644010904054144,
 0.014240209559025262,
 0.014827627847797042,
 0.018280913215721364,
 0.019121877404537562]

Large:


Mid:

[0.27945562084133735,
 0.17879796173281368,
 0.14067649967264556,
 0.12890605747331618,
 0.10475947497437907,
 0.13823494507685608,
 0.12353034935699093,
 0.11823404448760805,
 0.0729309362573854,
 0.14235647834799478]

Small:


Micro:


Nano:

This result provides some interesting insight. While there are patterns in micro to mega stocks, the patterns are different and not necessarily statistically significant. Large-cap and mid-cap stocks have a familiar U shape P/E ratio, which Fama and French predicted. However, small- and micro-cap stocks seem to have a linear relationship, and nano-cap stocks have what appears to be a random relationship.

Let’s look at each segment, starting with mega-cap stocks. From these numbers, there seems to be a pretty clear positive relationship between P/E and returns. That is, the higher the P/E ratio, the more the returns. This is the opposite of what Fama and French predict.

Large-cap stocks

0

No Comments

No comments yet

Leave a Reply

Your email address will not be published.