Monte Carlo

Foreword

How much money can you spend every year, if you want your money to last 30 years?

Does this change if you invest 100% in stocks? 100% in bonds? 60/40 in stocks/bonds?

Do you even know what the historical returns of stocks are?

As usual, a reminder that I am not a financial professional by training — I am a software engineer by training, and by trade. The following is based on my personal understanding, which is gained through self-study and working in finance for a few years.

If you find anything that you feel is incorrect, please feel free to leave a comment, and discuss your thoughts.

Raw data

Here is the annual total return (1) of the S&P500 index since 1926, taken from https://www.slickcharts.com/sp500/returns. I haven’t verified the data, but a quick glance suggests it’s probably close enough for our purposes. I’ve excluded 2021 because the year hasn’t ended yet. All numbers are in percentage terms.

  18.40,  # 2020
  31.49,  # 2019
  -4.38,  # 2018
  21.83,  # 2017
  11.96,  # 2016
  1.38,  # 2015
  13.69,  # 2014
  32.39,  # 2013
  16.00,  # 2012
  2.11,  # 2011
  15.06,  # 2010
  26.46,  # 2009
  -37.00,  # 2008
  5.49,  # 2007
  15.79,  # 2006
  4.91,  # 2005
  10.88,  # 2004
  28.68,  # 2003
  -22.10,  # 2002
  -11.89,  # 2001
  -9.10,  # 2000
  21.04,  # 1999
  28.58,  # 1998
  33.36,  # 1997
  22.96,  # 1996
  37.58,  # 1995
  1.32,  # 1994
  10.08,  # 1993
  7.62,  # 1992
  30.47,  # 1991
  -3.10,  # 1990
  31.69,  # 1989
  16.61,  # 1988
  5.25,  # 1987
  18.67,  # 1986
  31.73,  # 1985
  6.27,  # 1984
  22.56,  # 1983
  21.55,  # 1982
  -4.91,  # 1981
  32.42,  # 1980
  18.44,  # 1979
  6.56,  # 1978
  -7.18,  # 1977
  23.84,  # 1976
  37.20,  # 1975
  -26.47,  # 1974
  -14.66,  # 1973
  18.98,  # 1972
  14.31,  # 1971
  4.01,  # 1970
  -8.50,  # 1969
  11.06,  # 1968
  23.98,  # 1967
  -10.06,  # 1966
  12.45,  # 1965
  16.48,  # 1964
  22.80,  # 1963
  -8.73,  # 1962
  26.89,  # 1961
  0.47,  # 1960
  11.96,  # 1959
  43.36,  # 1958
  -10.78,  # 1957
  6.56,  # 1956
  31.56,  # 1955
  52.62,  # 1954
  -0.99,  # 1953
  18.37,  # 1952
  24.02,  # 1951
  31.71,  # 1950
  18.79,  # 1949
  5.50,  # 1948
  5.71,  # 1947
  -8.07,  # 1946
  36.44,  # 1945
  19.75,  # 1944
  25.90,  # 1943
  20.34,  # 1942
  -11.59,  # 1941
  -9.78,  # 1940
  -0.41,  # 1939
  31.12,  # 1938
  -35.03,  # 1937
  33.92,  # 1936
  47.67,  # 1935
  -1.44,  # 1934
  53.99,  # 1933
  -8.19,  # 1932
  -43.34,  # 1931
  -24.90,  # 1930
  -8.42,  # 1929
  43.61,  # 1928
  37.49,  # 1927
  11.62,  # 1926

Pop quiz 1

You can refer to the numbers above, but don’t use a calculator or anything, just try to estimate the answers:

  1. What do you think is the average annual return over the period covered?
  2. What do you think is the median annual return over the period covered?
  3. What do you think is the CAGR(2) over the period covered?
  4. Which of the following 3 numbers above should you use, if you want to estimate how much returns you’ll get over the next 10 years?

The answers are:

  1. 12.2%
  2. 14.3%
  3. 10.3%
  4. CAGR, because total returns over a period of time compounds multiplicatively. Average and median are non-compounding measures.

Are those numbers surprising? Most people find it surprising that the CAGR is so much lower than the other 2 measures, because they generally hear about the average/median returns thrown around in the media, but CAGR is a number that’s less frequently used, even though it’s more important. Some people, especially those who started investing since 2010, may find it surprising that the numbers are so low, because they are used to 15+% returns, in most years since 2010. However, because returns compound multiplicatively, a down year dramatically skews the CAGR, which is why we see these numbers.

Pop quiz 2

Now, let’s say we have some amount of money, $R, to retire on, and assume inflation rate of 3% (3), which is to say, if you need $X in year one, you’ll need $(1.03 * X) in year 2, and $(1.03^2 * X) in year 3, and so on.

The ratio X/R is your withdrawal rate in the first year. The safe withdrawal rate (or SWR) is the ratio X/R, such that you have a high probability of not running out of money within your retirement — in our case, 30 years.

Assuming we used $R to buy the S&P 500 index on day 1 of our retirement, and ignoring transaction costs, taxes, etc.,

  1. What do you think is the SWR if you want a 90% probability of not running out of money in 30 years?
  2. What about if we want a 95% probability?
  3. 99% probability?

Now, if you are like most people, you’ll probably do something like take average/median/CAGR of stocks return, subtract the inflation rate, and that’s your SWR. That’ll give you a number that is either 9.2%, 11.3% or 7.3%, depending on which measure of stocks return you used.

And all 3 answers are wrong. The correct answers are:

  1. 3.8%
  2. 3.1%
  3. 1.9%

Hopefully, you are surprised (4).

Whadafuqjuzhappened!?

The reason the numbers are so small, is because of volatility. Stocks don’t go up in a straight line, they often take little detours where the annual total returns is the wrong shade of green (5). During the years where stocks are down, you are actually spending a much larger percentage of your assets to maintain your lifestyle — since your spending strictly goes up due to inflation, X/R (or your withdrawal rate) goes up if X goes up and R goes down.

So, if you want to maintain the same lifestyle over time, you’ll need to start off by just withdrawing a smaller portion of your portfolio in the first year, i.e.: a lower SWR, to compensate for these episodic underperformance of stocks.

This is sometimes called “sequencing risk”.

I am never gonna retire

Well, maybe don’t despair yet — it’s not as bad as it sounds. Recall that you don’t have to invest (just) in stocks. You can also invest in bonds! Or real estate! Or fine art! Or in this blog! I take donations! (6)

Now, as we know, bonds have, in recent history, really low yields. At the time of this writing, the 30y US Treasury is yielding only 1.93%. Can bonds really help?

Pop quiz 3

Let’s say you use your entire retirement fund of $R to buy a 30y US Treasury yielding 2% (7). So, what do you think your SWR is for

  1. 90% probability of not running out of money in 30 years?
  2. 95% probability?
  3. 99% probability?

And the answers are… 2.9%. For all 3. Note that we are assuming US Treasuries won’t default, and you’ll always get your money back, on top of all the other assumptions above.

So yea, for 90/95%, it’s not as good as stocks, but the stability of bonds help in the 99% case.

What’s going on here?

Recall that I said the main reason why SWR for stocks is so low, is because of volatility and sequencing risk — you need money every year to survive, even if the stock market is being uncooperative. But bonds, being so helpfully stable (at least in our made up model world with semi-unrealistic assumptions), means that even at 99% (and 100%!) percentile levels, we can have the same SWR of 2.9%, higher, in fact, than their CAGR (which is 2%)!

To hammer home this point, I ran simulations of various scenarios, and the results are summarized below:

PortfolioAverage returnMedian returnCAGRSWR 90%SWR 95%SWR 99%
Sampled stocks12.14%13.91%10.32%3.75%3.00%1.83%
Normalized stocks12.17%12.14%10.43%4.01%3.25%2.07%
Low vol stocks12.14%12.14%11.72%6.49%6.00%5.18%
Low vol, low return stocks6.06%6.08%5.62%3.25%2.90%2.36%
High vol, high return stocks24.34%24.42%14.30%2.77%1.17%Impossible
2% bonds2.00%2.00%2.00%2.88%2.88%2.88%
6% bonds6.00%6.00%6.00%4.90%4.90%4.90%
10% bonds10.00%10.00%10.00%7.39%7.39%7.39%
60/40 stocks, 2% bonds8.05%8.07%7.42%3.79%3.35%2.66%
55/35/10 stocks, 2% bonds, cash7.35%7.36%6.82%3.66%3.24%2.62%
60/40 stocks, 2% bonds, 1.5x 1% margin11.62%11.65%10.22%4.25%3.54%2.49%
60/40 stocks, 2% bonds, 1.5x 6% margin9.11%9.05%7.67%3.08%2.51%1.61%

“Sampled stocks” is stocks using actual historical returns, uniformly sampled for each simulation year.

“Normalized stocks” is stocks using a random returns sampled from a normal distribution with 12.16% mean and 19.66% standard deviation (which is the mean/standard deviation of our historical data above). As you can see, these numbers are fairly similar. Because it’s easier to model different scenarios using a normal distribution, all other simulations involving stocks use variations of “normalized stocks”.

“Low vol stocks” is stocks where we simply halved the standard deviation for modeling purposes. “Low vol, low return stocks” is stocks where we halved both the standard deviation and the mean. “High vol, high return stocks” is stocks where we doubled both standard deviation and mean.

“2/6/10% bonds” are bonds where the yield is 2%, 6% or 10%. (8)

The remaining rows show composite portfolios where we have some percentage of assets in stocks, bonds or cash, and where we may apply leverage (buying 50% of the portfolio’s value on margin) at different margin interest rates.

Observations

If you go through the data carefully, you’ll quickly see that:

  1. Expected returns (either via average, median or CAGR) is not a good predictor of SWR at all, especially at the higher confidences.
  2. Instead, volatility, or lack thereof, is a much better predictor of SWR, again, especially at the higher confidences.
  3. So, you can sacrifice some expected returns, and get a higher SWR 99% rate, by swapping out some stocks for bonds, or even cash!
  4. If you can get cheap leverage, then some mild application of leverage on a balanced portfolio (for example, 60/40 1.5x leverage with 1% margin) can yield even better results.
  5. But using leverage without first tamping down volatility is a recipe for disaster (not shown here, but the high vol, high return stocks scenario is a good approximate).

Wrapping up

For a very long time, people have been asking me why I’m “leaving money on the table” by not being more aggressive in stocks, or why I’m not levering 100% into stocks, etc. Some have even suggested a portfolio of 100% UPRO (which is a 3x daily balanced SPY product).

But think of it this way — when you retire, you’ll depend essentially 100% on your portfolio for cashflow to survive. And as we discussed in “net worth“, net worth is only useful if it can be used somehow to generate cash flow. Because, say it with me now, you cannot eat net worth. Therefore, “expected net worth”, based on whatever modeling of expected returns from a risky portfolio, is only useful if I can depend on it, at retirement, to generate cash flow. It doesn’t matter if the expected value of my portfolio is $1B at retirement, if there’s a 50% probability I’d go bankrupt — What? Am I supposed to eat caviar on my mega yacht off Monaco 50% of the time and then jump off a building the other 50%? (9)

What about levering up now and then selling everything at retirement to buy safer assets? Sure, if you happen to retire when the stock markets are at a high. But I’m not inclined to time my retirement based on the whims of the stock market. Also, since I don’t have a crystal ball, that means I’ll have to go with a more conservative strategy.

Which is to say, in general, as you approach retirement, it is a good idea to reduce volatility in your portfolio, so that you can smooth out market madness and thus achieve a higher level of stable cash flow (higher SWR) from your portfolio. (10)

Monte Carlo

By now, you’re probably wondering why this post is titled “Monte Carlo”. That’s simply the name of the methodology I used to run the simulation for the numbers above. The code for the simulation is attached, feel free to play with the assumptions yourself to see what comes up.

Note that for all the stocks based portfolios, the inputs are random (which is why we need Monte Carlo in the first place), so your numbers may differ slightly. But I’ve found that the differences are relatively minor, typically in the 5-10bps range.

#!/usr/bin/python3.8

import numpy


# Number of times to run each simulation.
TOTAL_ITERATIONS = 10000
# Ratio of runs where we must end up with more than $0, before we consider the test a success.
THRESHOLDS = [0.9, 0.95, 0.99, 1]

# Number of years to run for in each simulation.
NUM_YEARS = 30
# Inflation rate of cash withdrawal.
INFLATION = 0.03

# This should be mostly irrelevant.  Just use a large number.
START_CASH = 1000000

# Data from https://www.slickcharts.com/sp500/returns
HISTORICAL_RETURNS = numpy.array([
  18.40,  # 2020
  31.49,  # 2019
  -4.38,  # 2018
  21.83,  # 2017
  11.96,  # 2016
  1.38,  # 2015
  13.69,  # 2014
  32.39,  # 2013
  16.00,  # 2012
  2.11,  # 2011
  15.06,  # 2010
  26.46,  # 2009
  -37.00,  # 2008
  5.49,  # 2007
  15.79,  # 2006
  4.91,  # 2005
  10.88,  # 2004
  28.68,  # 2003
  -22.10,  # 2002
  -11.89,  # 2001
  -9.10,  # 2000
  21.04,  # 1999
  28.58,  # 1998
  33.36,  # 1997
  22.96,  # 1996
  37.58,  # 1995
  1.32,  # 1994
  10.08,  # 1993
  7.62,  # 1992
  30.47,  # 1991
  -3.10,  # 1990
  31.69,  # 1989
  16.61,  # 1988
  5.25,  # 1987
  18.67,  # 1986
  31.73,  # 1985
  6.27,  # 1984
  22.56,  # 1983
  21.55,  # 1982
  -4.91,  # 1981
  32.42,  # 1980
  18.44,  # 1979
  6.56,  # 1978
  -7.18,  # 1977
  23.84,  # 1976
  37.20,  # 1975
  -26.47,  # 1974
  -14.66,  # 1973
  18.98,  # 1972
  14.31,  # 1971
  4.01,  # 1970
  -8.50,  # 1969
  11.06,  # 1968
  23.98,  # 1967
  -10.06,  # 1966
  12.45,  # 1965
  16.48,  # 1964
  22.80,  # 1963
  -8.73,  # 1962
  26.89,  # 1961
  0.47,  # 1960
  11.96,  # 1959
  43.36,  # 1958
  -10.78,  # 1957
  6.56,  # 1956
  31.56,  # 1955
  52.62,  # 1954
  -0.99,  # 1953
  18.37,  # 1952
  24.02,  # 1951
  31.71,  # 1950
  18.79,  # 1949
  5.50,  # 1948
  5.71,  # 1947
  -8.07,  # 1946
  36.44,  # 1945
  19.75,  # 1944
  25.90,  # 1943
  20.34,  # 1942
  -11.59,  # 1941
  -9.78,  # 1940
  -0.41,  # 1939
  31.12,  # 1938
  -35.03,  # 1937
  33.92,  # 1936
  47.67,  # 1935
  -1.44,  # 1934
  53.99,  # 1933
  -8.19,  # 1932
  -43.34,  # 1931
  -24.90,  # 1930
  -8.42,  # 1929
  43.61,  # 1928
  37.49,  # 1927
  11.62,  # 1926
])
HISTORICAL_RETURNS /= 100

# Uncomment to print average, median and CAGR of HISTORICAL_RETURNS.
#print(numpy.mean(HISTORICAL_RETURNS) * 100)
#print(numpy.median(HISTORICAL_RETURNS) * 100)
#print((numpy.prod(HISTORICAL_RETURNS + 1) ** (1 / len(HISTORICAL_RETURNS)) - 1) * 100)


class Sim():
  def __init__(self, label):
    self.__label = label

  def Name(self):
    return self.__label


class FixedRate(Sim):
  def __init__(self, label, interest_rate):
    Sim.__init__(self, label)
    self.__interest_rate = interest_rate

  def Return(self):
    return self.__interest_rate


class NormalDistribution(Sim):
  def __init__(self, label, mean, std_dev):
    Sim.__init__(self, label)
    self.__mean = mean
    self.__std_dev = std_dev

  def Return(self):
    return max(-1, numpy.random.normal(self.__mean, self.__std_dev))


class UniformSampling(Sim):
  def __init__(self, label, data):
    Sim.__init__(self, label)
    self.__data = data

  def Return(self):
    return numpy.random.choice(self.__data)


class Cash(FixedRate):
  def __init__(self, label):
    FixedRate.__init__(self, label, 0)


class FullLoss(FixedRate):
  def __init__(self, label):
    FixedRate.__init__(self, label, -1)


class Composite(Sim):
  def __init__(self, label, *args):
    Sim.__init__(self, label)
    self.__args = args

  def Return(self):
    result = 0
    for asset, ratio in self.__args:
      result += asset.Return() * ratio
    return result


def RunOneIteration(model, rate, returns):
  value = START_CASH
  required_cash = START_CASH * rate
  for i in range(NUM_YEARS):
    if value < required_cash:
      return False
    value -= required_cash
    value *= 1 + returns[i]
    required_cash *= (1 + INFLATION)
  return True


def RunSim(threshold, model, rate, returns):
  num_pass_required = TOTAL_ITERATIONS * threshold
  for i in range(TOTAL_ITERATIONS):
    if RunOneIteration(model, rate, returns[i]):
      num_pass_required -= 1
      if num_pass_required <= 0:
        return True
  return False


def GenerateReturns(model):
  output = numpy.empty([TOTAL_ITERATIONS, NUM_YEARS])

  for i in range(TOTAL_ITERATIONS):
    curr_results = output[i]
    for j in range(NUM_YEARS):
      curr_results[j] = model.Return()

  return output


def Report(model, output):
  print("{}:".format(model.Name()))

  while output:
    prefix = output[:5]
    output = output[5:]
    print("  " + "  ".join(["{:>8s}: {:<7s}".format(metric, "{:.2f}%".format(result * 100)) for (metric, result) in prefix]))
  print()



def MonteCarlo(model):
  returns = GenerateReturns(model)

  highest_rate = {}
  for threshold in THRESHOLDS:
    highest_rate[threshold] = float("nan")
    min_rate = 0
    max_rate = 1
    while round(min_rate, 4) < round(max_rate, 4):
      rate = (min_rate + max_rate) / 2
      if RunSim(threshold, model, rate, returns):
        min_rate = rate
        highest_rate[threshold] = rate
      else:
        max_rate = rate

  output = [
    ("Average", numpy.mean(numpy.mean(returns, axis=1))),
    ("Median", numpy.mean(numpy.median(returns, axis=1))),
    ("CAGR", numpy.mean(numpy.prod(returns + 1, axis=1) ** (1 / NUM_YEARS) - 1)),
  ]

  for threshold, rate in highest_rate.items():
    output.append(("SWR-{}%".format(int(threshold * 100)), rate))

  Report(model, output)


def MakeLabelParams(label, *params):
  full_label = label
  first = True
  for param in params:
    if first:
      first = False
      full_label += " {:.2f}".format(param * 100)
    else:
      full_label += "/{:.2f}".format(param * 100)

  return full_label, *params


def Main():
  cash = Cash("Cash")
  margin = FullLoss("MarginCost")

  bonds2 = FixedRate("2% Bonds", 0.02)
  MonteCarlo(bonds2)

  bonds6 = FixedRate("6% Bonds", 0.06)
  MonteCarlo(bonds6)

  bonds10 = FixedRate("10% Bonds", 0.1)
  MonteCarlo(bonds10)

  sampled_stocks = UniformSampling("SampledStocks", HISTORICAL_RETURNS)
  MonteCarlo(sampled_stocks)

  stocks_mean = numpy.mean(HISTORICAL_RETURNS)
  stocks_stdev = numpy.std(HISTORICAL_RETURNS, ddof=1)
  stocks = NormalDistribution(*MakeLabelParams("Stocks", stocks_mean, stocks_stdev))
  MonteCarlo(stocks)

  low_vol = NormalDistribution(*MakeLabelParams("Stocks[LoVol]", stocks_mean, stocks_stdev * 0.5))
  MonteCarlo(low_vol)

  low_vol_mean = NormalDistribution(*MakeLabelParams("Stocks[LoVolMean]", stocks_mean * 0.5, stocks_stdev * 0.5))
  MonteCarlo(low_vol_mean)

  high_vol_mean = NormalDistribution(*MakeLabelParams("Stocks[HiVolMean]", stocks_mean * 2, stocks_stdev * 2))
  MonteCarlo(high_vol_mean)

  stocks60_bonds240 = Composite("60/40", (stocks, 0.6), (bonds2, 0.4))
  MonteCarlo(stocks60_bonds240)

  stocks55_bonds235_cash10 = Composite("55/35/10", (stocks, 0.55), (bonds2, 0.35), (cash, 0.1))
  MonteCarlo(stocks55_bonds235_cash10)

  # 50% margin loan, at 1% rate = 0.05% interest payments per year.
  stocks60_bonds240_x15 = Composite("60/40 x1.5", (stocks, 0.9), (bonds2, 0.6), (margin, 0.005))
  MonteCarlo(stocks60_bonds240_x15)

  # 50% margin loan, at 6% rate = 3% interest payments per year.
  stocks60_bonds240_x15 = Composite("60/40 x1.5", (stocks, 0.9), (bonds2, 0.6), (margin, 0.03))
  MonteCarlo(stocks60_bonds240_x15)


if __name__ == "__main__":
  Main()

Footnotes

  1. Total return is equal to dividends + gains in asset price.
  2. CAGR is “compounded annual growth rate”, which is, loosely speaking, the geometric mean of the returns, expressed in percentage terms.
  3. I’ve been using 3% for modeling inflation for a while now. People used to laugh at me for this, especially during the 2009-2019 period. After 2020, they are still laughing at me. But for very different reasons. For those who are more conservative, feel free to jack up the value to 5% (or more!) in the simulation to see how that affects the numbers.
  4. Look, buddy. I worked really hard to build up the suspense and everything. At least act surprised.
  5. Also known as “red”.
  6. I’m kidding, I don’t take donations.
  7. I’m using 2% because it’s easier to type than 1.93%. Also, US Treasuries have some tax advantages, so it’s probably not THAT crazy an assumption. Finally, there are other “safe’ish” assets that can yield as high as 5-6% “safely”.
  8. Yes, 6% and 10% bonds sound crazy in today’s low interest rates world. But there are assets (mostly for accredited investors) which can indeed yield up to 12%. They have varying degrees of risk, and certainly aren’t risk free like this modeling suggests. But they behave very similarly to bonds.
  9. Interestingly, someone made the argument to me recently that if there’s an asset with a 25% probability of 10x, and 75% probability of going to 0, the expected value is still 2.5x, which means (paraphrased) “you almost have an obligation to buy that asset”. Hopefully this post and the discussion have shown that the question (and thus answer) is not so simple, and that there are a lot of other considerations other than “expected return”.
  10. There are other withdrawal strategies that try to mitigate the sequencing risk issue. Most of them revolve around reducing your cash flow in bad market years (e.g.: keeping your withdrawal rate the same, or even reducing it). Some of them may work, but in general, I’m not really inclined to eat lobster one year and starve the next, just because the stock market decides to tank. I’d rather just have my daily, stable supply of ramen noodle.

Leave a comment