Introduction

Equity valuation may be a predictive signal for future Equity return. There are various methodologies to evaluate whether the Equity is undervalued or overvalued using metrics like price-to-earnings (P/E), return on equity (ROE), dividend yield, book-to-equity and so on. In this algorithm, we use a ten-year normalized earnings metrics invented by Yale University professor Robert Shiller to find the fair value of the Equity market.

Method

The cyclically adjusted price-to-earnings ratio (CAPE) compares the stock prices with earnings smoothed across multiple years. It is the price divided by the average of ten years of earnings (moving average), adjusted for inflation. The backward-looking earnings smooth out the economic cycle as well as the price fluctuations.

The investment universe consists of 26 countries with easily accessible Equity markets via ETFs. We import the custom CAPE ratio (Shiller PE Ratio) data of those 26 countries and create a dictionary to save the corresponding country ETF. This custom data is from Barclays Indices is in monthly resolution and starts January 1982.

class CAPE(PythonData):

    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("https://indices.cib.barclays/file.app?action=shared&path=shiller/cape.csv", SubscriptionTransportMedium.RemoteFile)

    def Reader(self, config, line, date, isLiveMode):
        if not (line.strip() and line[1].replace('.', '', 1).isdigit()): return None

        try:
            index = CAPE()
            index.Symbol = config.Symbol

            data = line.split(',')
            index.Time = datetime.strptime(data[0], "%d/%m/%Y")
            symbols = Symbols().tickers
            for key, value in symbols.items():
                index[key] = float(data[value[0]]) if data[value[0]] else 100000 # very large number to avoid be selected
            return index

        except:
            return None

class Symbols:
    def __init__(self):
        # the indiex is the country name
        # the first element of the value is the column number of CAPE ratio value in custom dataset
        # the second element of the value is the corresponding country ETF

        self.tickers = {"Australia":[1, "EWA"],       # ASX All Ordinaries Index: iShares MSCI Australia ETF
                        "Brazil":[2, "EWZ"],          # Indice Bovespa (Ibovespa): iShares MSCI Brazil ETF
                        "Canada":[3, "XIC"],          # S&P/TSX Composite Index: iShares S&P TSX Capped Cmpst Indx Fnd
                        "China":[4, "MCHI"],          # SSE Composite: iShares MSCI China Index Fund
                        "Europe":[5, "IEUR"],         # STOXX Europe 600 Index: iShares Core MSCI Europe ETF
                        "France":[6, "EWQ"],          # CAC 40 Index: iShares MSCI France ETF
                        "Germany":[7, "EWG"],         # HDAX Index: iShares MSCI Germany ETF
                        "Hong Kong":[8, "EWH"],       # Hang Seng Index: iShares MSCI Hong Kong Index Fund
                        "Italy":[9, "EWI"],           # FTSE MIB Index: iShares MSCI Italy ETF
                        "India":[10, "INDY"],         # NIFTY 50 Index: iShares India 50 ETF
                        "Israel":[11, "EIS"],         # Tel Aviv 125 Index: iShares MSCI Israel ETF
                        "Japan":[12, "EWJ"],          # All Public Companies: iShares MSCI Japan ETF
                        "Korea":[13,"EWY"],           # KOSPI Index: iShares MSCI South Korea ETF
                        "Mexico":[14, "EWW"],         # &P/BMV IPC Index: iShares MSCI Mexico ETF
                        "Netherlands":[15, "EWN"],    # NL 25 Index: iShares MSCI Netherlands ETF
                        "Poland":[16, "EPOL"],        # WIG Index: iShares MSCI Poland ETF  
                        "Russia":[17, "ERUS"],        # RTS Index: iShares MSCI Russia ETF
                        "Singapore":[18, "EWS"],      # STI Index:  iShares MSCI Singapore ETF
                        "Southafrica":[19, "EZA"],    # FTSE/JSE CAP Top 40 Index: iShares MSCI South Africa ETF
                        "Spain":[20, "EWP"],          # IBEX 35 Index: iShares MSCI Spain ETF
                        "Sweden":[21, "EWD"],         # OMXS 30 index: iShares MSCI Sweden ETF
                        "Switzerland":[22, "EWL"],    # CH 20 index: iShares MSCI Switzerland ETF
                        "Taiwan":[23, "EWT"],         # TWSE: iShares MSCI Taiwan ETF
                        "Turkey":[24, "TUR"],         # BIST 100: iShares MSCI Turkey ETF
                        "UK":[25, "EWU"],             # FTSE 100 Index: iShares MSCI United Kingdom ETF
                        "USA":[26, "SPY"]            # S&P 500 Index: SPDR S&P 500 ETF 
        }

According to the academic research of Shiller and Campbell, using market data from the S&P index, the lower the CAPE, the higher the investors' likely return from Equities. Therefore, the algorithm then invests in the cheapest 33% of countries from the sample with the lowest CAPE ratio if those countries have a CAPE below 15. If there are no countries with CAPE lower than 15, the algorithm holds cash instead of country ETFs. The portfolio is equally weighted and rebalanced monthly by every update of the CAPE data.

def OnData(self, data):
    if data.ContainsKey(self.symbol):
        cape = {}
        for key, value in self.symbols.items(): 
            cape[value[1]] = data[self.symbol].GetProperty(key)

        sorted_cape = sorted(cape, key = lambda x: cape[x])

        # invests the cheapest 33% of countries if those countries have a CAPE below 15
        lowest_cape = sorted_cape[:int(1/3*len(sorted_cape))]
        long_list = [i for i in lowest_cape if cape[i] < 15]
        invested = [x.Key for x in self.Portfolio if x.Value.Invested]

        for i in invested:
            if i.Value not in long_list:
                self.Liquidate(i)

        for i in long_list:
            self.SetHoldings(i, 1/len(long_list))


Reference

  1. Quantpedia - Value Effect within Countries
  2. Barclays Indices - Historic CAPE Ratio by country