Miguel Vilá

Stopping when Robert Shiller tells you to

Nov 08 2022

“If these trends continue… aaay!”

Disco Stu

Obligatory disclaimer: This is not financial advice. The information presented here does not constitute any form of advice or recommendation on my part. Seek independent financial advice on your own.

Anyways

Let’s say you are investing in an index fund tracking the S&P 500. Each month you invest some amount, let’s say $USD 500: you buy as many S&P 500 units as this can afford you. You do this no matter what the price is. This simple strategy is called cost averaging and it follows this idea that you can’t time the market. What is more important is “time in the market”, not “timing the market”, the saying goes.

But, what if you start thinking that maybe things are getting a bit dumb. Maybe you see some 49 year old CEO posting memes or maybe you hear people comparing the stock market and astrology unironically (I love her, by the way).

Instead of following the cost averaging strategy, every month you could ask “Is the market expensive at the moment?” and then, if it is, save that money for a while, until the market gets reasonable again. We will start with some data.

S&P 500 Data

We will be looking at the historic value of the S&P 500, which is a stock market index that tracks the performance of 500 large companies listed on stock exchanges in the United States. It is a good proxy for the US stock market, and several investment services offer ETFs that track it.

Here’s a logarithmic graph showing its value from 1920 to 2021:

In gray there are some recessions/interesting events.

We will first evaluate cost-averaging. For this, we will run some simulations.

If you are not interested in looking at the code you can jump here.

Simulating Cost Averaging

Let’s simulate the simple cost averaging strategy.

We will write a function that applies this strategy to a period of time, which should contain monthly S&P Value data. The next function returns two things:

deposit_per_month = 500

def cost_average_monthly_strategy(monthly_data):
    total_acquired_units = (deposit_per_month / monthly_data["Value"]).sum()
    total_deposits = deposit_per_month * len(monthly_data)
    return total_acquired_units, total_deposits

Here deposit_per_month / monthly_data["Value"] is a vector, indicating the number of S&P units acquired each month. The total sum would represent the number of units acquired by the end of the investment horizon.

Now, we could iteratively apply this function starting at different months to evaluate the final annualized rate of return. We will parametrize this by different arguments:

"""
Given an strategy and some data simulates an investing for
a time horizon of `inv_years` and an initial date of `date_start`.

Returns:
- the annualized rate of return
- the investment end date (the date at which the units are redeemed)
"""
def simulate_investing_for_period(inv_years, date_start, data, investment_strategy):
    inv_end_date = date_start + relativedelta(years=inv_years)
    investment_range = data[(date_start <= data["Date"]) & (data["Date"] <= inv_end_date)]
    total_units, total_deposits = investment_strategy(investment_range)
    final_units_value = total_units * investment_range["Value"].iat[-1]
    inv_return = ( final_units_value / total_deposits ) ** (1 / inv_years) - 1
    return inv_return, inv_end_date

"""
Simulates an investing given an horizon of `inv_years`,
an `investment_strategy` function (e.g. `cost_average_monthly_strategy`),
and some `data` used to calculate the dates and passed to the strategy.

Returns an array with `Return` and `RedemptionDate` columns, which indicates
the annualized rate of return of the strategy when the redeeming the units at
that date.
"""
def simulate_investment(inv_years, investment_strategy, data):
    up_to_date = max(data["Date"]) - relativedelta(years=inv_years)
    inv_dates = data[data["Date"] <= up_to_date]["Date"]
    inv_sim = inv_dates.map(lambda date_start: 
                            simulate_investing_for_period(
                                inv_years, 
                                date_start, 
                                data, 
                                investment_strategy
                            )
                           ).tolist()
    inv_sim = pd.DataFrame(inv_sim, columns=['Return','RedemptionDate'])
    return inv_sim

And that’s a good start. With simulate_investment we can simulate an investment of x number of years, using some investment strategy and based on some historic data. This parametrization will be useful when comparing different strategies and time horizons.

Cost Averaging Evaluated

Let’s see the annualized return of Cost averaging $500 USD per month with different time horizons.

These graphs show the annualized investment return of cost averaging the S&P 500 for the last x years, from 1920 to February 2021:

These graphs don’t show anything overly interesting. A few things to note:

CAPE and Excess CAPE Yield

Let’s come back to our original question. Instead of following that simple strategy, would it be worth considering if the market is expensive?

Fortunately, a smart guy came up with a way to tell if a market is overpriced or not. This is Robert Schiller’s Cyclically Adjusted Price to Earnings Ratio, CAPE for short. It’s the Price to Earnings ratio, adjusted to business cycles (averaged over the last 10 years) and taking into consideration inflation. You can read a better description of the CAPE ratio in this article. Here’s a summary:

The way it works is that you take the average of the last ten years of earnings, adjust them for inflation, and divide the current index price by that adjusted earnings. This makes it so that the current price is divided by the average earnings over the latest business cycle rather than just one recent year of bad or good earnings.

You can even download a monthly updated version of Shiller’s CAPE ratio from his own website.

Let’s play with that data using Python, pandas, numpy, and plotly.

Here’s a graph showing the S&P value, the CAPE ratio:

Some things to note:

Just before two stock market crashes the CAPE ratio reached very high levels. This was true for the 1929 stock market crash and for the DotCom bubble aftermath, but not for the Global Financial Crisis. I suppose that the Global Financial Crisis, which was caused by a housing market bubble, didn’t raise the CAPE ratio. Once the crisis consequences leaked into the stock market, you can see the CAPE ratio plummet, but there was no previous peak. In essence, the CAPE ratio was not a good predictor for the Global Financial Crisis, but it was for the DotCom bubble or the 1929 stock market crash.

Another thing to note, is that, as of February 2021 the CAPE ratio sits at around 34, a bit higher than the peak of the Great Depression.

Identifying when the market is overvalued

Our definition of an overvalued market is going to be a simple one: when the CAPE ratio is above the 90% historic percentile.

When would have we considered the market overvalued in the past? Let’s plot this:

There are a few instances:

Saving when the market is expensive

Let’s define a strategy that takes advantage of the CAPE ratio. If it’s above the 95%th percentile we don’t acquire units during that period of time and instead save it. Once the CAPE ratio falls below the 90%th percentile we use the saved money to buy units again.

In code:

cape_threshold_perc = 90
cape_threshold = np.percentile(shiller["CAPE"], cape_threshold_perc)

def cape_thresholding_yearly_strategy(yearly_data):
    total_units = 0.0
    saved_deposits = 0.0
    for i in yearly_data.index:
        if yearly_data["CAPE"][i] < cape_threshold:
            total_units += (12 * deposit_per_month + saved_deposits) / yearly_data["Value"][i]
            saved_deposits = 0.0
        else:
            saved_deposits += deposit_per_month * months
    return total_units, (12 * deposit_per_month * len(yearly_data))

Note: I’m using the yearly data here, because doing it monthly was very slow.

Comparing the strategies

Anyways, let’s get to the results. Here we compare the 2 strategies with different investment horizons:

Things to note:

This seems to suggest that this kind of strategy might be worth pursuing if you investment horizon is very short, but not much if it’s longer. This whole analysis is not very scientific or rigurous, but it seems to confirm some of the usual investment advice you hear.

If you want to see the full code written for this post you can find it in this Jupyter notebook. You will need to have installed the dependencies listed here