Original paper
Abstract
The price-earnings effect has been thoroughly documented and widely studied around the world. However, in existing research it has almost exclusively been calculated on the basis of the previous year's earnings. We show that the power of the effect has until now been seriously underestimated, due to taking too short-term a view of earnings. We look at all UK companies since 1975, and using the traditional P/E ratio we find the difference in average annual returns between the value and glamour deciles to be 6%, similar to other authors' findings. We are able to almost double the value premium by calculating P/E ratios using earnings averaged over the last eight years. Averaging, however, implies equal weights for each past year. We further enhance the premium by optimising the weights of the past years of earnings in constructing the P/E ratio.
Keywords:Â price-earning ratios, value investing, arbitrage strategy, UK stock market, value premium
Trading rules
- Scope of investment: Companies listed in the UK (barring financial firms and entities with various share classes).
- Annual assessment date: Every 1st of May.
- Determine CAPE ratio: Use current price and 8-year average normalized earnings.
- Sort stocks: Classify into ten groups by their CAPE ratio.
- Buy: Invest in stocks with the lowest CAPE ratio.
- Equally weighted, yearly rebalancing.
Python code
Backtrader
import datetime
import backtrader as bt
class CAPE(bt.Strategy):
params = (
('rebalance_date', (5, 1)), # Rebalance date: May 1st
('lookback_years', 8),
('n_deciles', 10),
('target_decile', 0), # Target lowest CAPE ratio decile
)
def __init__(self):
self.earnings = {}
self.cape_ratios = {}
def next(self):
if self._check_rebalance():
self._calculate_cape_ratios()
self._rebalance_portfolio()
def _check_rebalance(self):
rebalance_month, rebalance_day = self.params.rebalance_date
return self.data.datetime.date().month == rebalance_month and self.data.datetime.date().day == rebalance_day
def _calculate_cape_ratios(self):
for d in self.datas:
price = d.close[0]
earnings = self.earnings.get(d._name, [])
if len(earnings) >= self.params.lookback_years * 12:
avg_earnings = sum(earnings[-(self.params.lookback_years * 12):]) / (self.params.lookback_years * 12)
self.cape_ratios[d._name] = price / avg_earnings
def _rebalance_portfolio(self):
sorted_data = sorted(self.datas, key=lambda d: self.cape_ratios.get(d._name, float('inf')))
target_datas = sorted_data[:len(sorted_data) // self.params.n_deciles]
for d in self.datas:
if d in target_datas:
self.order_target_percent(d, target=1 / len(target_datas))
else:
self.order_target_percent(d, target=0)
def notify_fund(self, cash, value, fundvalue, shares):
self.earnings[self.data._name] = shares
Keep in mind that this code snippet assumes you have already prepared your data feeds and added them to the Cerebro engine. Additionally, the notify_fund
method assumes you have the earnings data of each stock in the shares
argument. You might need to adjust this depending on how your data feeds are structured.