Sliding-Window Investment Analysis

George Fisher

2016-05-09

Scenario

We are facing retirement, having to live off a lump sum we have invested. How much can we withdraw each year adjusted for inflation?

Parameters

* we are 65 years old   
* we expect to live for 30 years   
* we expect 2.5% annual inflation
* we expect to pay brokerage fees of 1.25%   
# function-specific parameters
library(sp500SlidingWindow)
window_width <- 30           # life expectancy
annual_fee   <- 0.0125       # brokerage fees
output_path  <- "images_rjournal/"

# analysis-specific parameters
current_age        <- 65      # starting age
annual_inflation   <- 0.025   # annual inflation
initial_investment <- 3000000 # our nest egg

# withdraw this much each year
withdrawal_percent <- 0.03171
initial_withdrawal <- initial_investment * withdrawal_percent       

Investment vector

We retire with this much money to live on for the rest of our ‘window_width’-year life

investment_vector <- c(initial_investment,
                       rep(0,window_width-1))

fmt(investment_vector)
#>  [1] "3,000,000" "        0" "        0" "        0" "        0"
#>  [6] "        0" "        0" "        0" "        0" "        0"
#> [11] "        0" "        0" "        0" "        0" "        0"
#> [16] "        0" "        0" "        0" "        0" "        0"
#> [21] "        0" "        0" "        0" "        0" "        0"
#> [26] "        0" "        0" "        0" "        0" "        0"

Withdrawal vector

We hope to spend the initial_withdrawal adjusted for inflation each year

withdrawal_vector <- sapply(0:(window_width-1), function(i) {
    return(initial_withdrawal * (1 + annual_inflation)**i)
})

# total amount we hope to withdraw over the whole period
total_hoped_for_wdr <- sum(withdrawal_vector)
fmt(total_hoped_for_wdr)
#> [1] "4,176,464"

# year-by-year hoped-for withdrawals
fmt(withdrawal_vector)
#>  [1] " 95,130" " 97,508" " 99,946" "102,445" "105,006" "107,631" "110,322"
#>  [8] "113,080" "115,907" "118,804" "121,774" "124,819" "127,939" "131,138"
#> [15] "134,416" "137,777" "141,221" "144,752" "148,370" "152,080" "155,882"
#> [22] "159,779" "163,773" "167,867" "172,064" "176,366" "180,775" "185,294"
#> [29] "189,927" "194,675"

Uninvested results

If we do not invest the lump sum but simply spend it down, what will be the result?

remaining_amt    <- initial_investment
ages             <- current_age:(window_width+current_age-1)
remaining_amount <- sapply(1:window_width, function(i) {
    remaining_amt <<- remaining_amt - withdrawal_vector[i]
    return(remaining_amt)
})

# test if we go below zero
if (length(which(remaining_amount<=0))) {
    # goes below zero
    plot_points = TRUE
    busted_age <- current_age+which(remaining_amount<=0)[1]-1
    sub = paste0("Run out of money at age ", busted_age)
} else {
    # does not go below zero
    plot_points = FALSE
    sub="Starting amount covers withdrawals without investment"
}

ylim <- c(min(min(remaining_amount, 0)), initial_investment)
plot(ages, remaining_amount, pch=20,
     xlab="Age", ylab=NA, yaxt="n", ylim=ylim,
     main="What if I simply live off the cash?",
     sub=sub)
if (plot_points) {
    points(which(remaining_amount<=0)+current_age-1, 
           remaining_amount[which(remaining_amount<=0)], col="red", pch=20)
    abline(v=busted_age, col="red", lty=2)
    abline(h=0, col="red", lty=2)
}

axis(2, las=2, at=axTicks(2), labels=fmt(axTicks(2)), cex.axis=0.72)
grid()


dev.copy(png, paste0(output_path, "invested_in_cash.png"))
#> png 
#>   3
dev.off()
#> png 
#>   2

Sliding Window Analysis

The idea of sliding-window analysis is to ask how a certain set of annual investments and withdrawals would perform in each of the periods of a certain width of the stock market.

We are simulating a brokerage cash account so the balance can never go below zero. If the withdrawal vector calls for an amount greater than the current balance, the remaining balance is withdrawn and the balance is set to zero.

The analysis invests the lump sum at the beginning of each window and tracks the effect of the stock market on that investment, making inflation-adjusted withdrawals each year. The critical question is whether the investor will run out of money.

window_df <- sp500SlidingWindow(investment_vector,
                                withdrawal_vector,
                                window_width = window_width,
                                annual_fee   = annual_fee,
                                output_path  = output_path)

knitr::kable(window_df)
start_year end_year ending_bal inv wdr IRR
1950 1979 7149639.3 3e+06 4176464 0.0626928
1951 1980 6219105.9 3e+06 4176464 0.0595528
1952 1981 4503118.7 3e+06 4176464 0.0528215
1953 1982 4318120.4 3e+06 4176464 0.0520039
1954 1983 6325965.5 3e+06 4176464 0.0599291
1955 1984 2473794.5 3e+06 4176464 0.0424521
1956 1985 1353134.1 3e+06 4176464 0.0346914
1957 1986 1765082.5 3e+06 4176464 0.0377963
1958 1987 3887384.6 3e+06 4176464 0.0500150
1959 1988 1124767.1 3e+06 4176464 0.0328746
1960 1989 1065790.4 3e+06 4176464 0.0323839
1961 1990 2175340.1 3e+06 4176464 0.0405756
1962 1991 391120.0 3e+06 4176464 0.0259948
1963 1992 3184183.4 3e+06 4176464 0.0464591
1964 1993 1262375.1 3e+06 4176464 0.0339902
1965 1994 448962.2 3e+06 4176464 0.0265775
1966 1995 0.0 3e+06 4146212 0.0211432
1967 1996 4376908.0 3e+06 4176464 0.0522660
1968 1997 2147757.4 3e+06 4176464 0.0403963
1969 1998 2097237.5 3e+06 4176464 0.0400654
1970 1999 9612337.1 3e+06 4176464 0.0697932
1971 2000 11801340.3 3e+06 4176464 0.0750960
1972 2001 8511201.0 3e+06 4176464 0.0668055
1973 2002 4210147.6 3e+06 4176464 0.0515169
1974 2003 13025578.5 3e+06 4176464 0.0777060
1975 2004 31097011.9 3e+06 4176464 0.1031360
1976 2005 22342913.8 3e+06 4176464 0.0930246
1977 2006 20572001.2 3e+06 4176464 0.0905808
1978 2007 29929683.3 3e+06 4176464 0.1019452
1979 2008 19225805.6 3e+06 4176464 0.0886035
1980 2009 22472955.7 3e+06 4176464 0.0931976
1981 2010 18215461.8 3e+06 4176464 0.0870432
1982 2011 23534522.9 3e+06 4176464 0.0945794
1983 2012 24161280.6 3e+06 4176464 0.0953708
1984 2013 26142715.6 3e+06 4176464 0.0977636
1985 2014 31212278.0 3e+06 4176464 0.1032515
1986 2015 23435771.6 3e+06 4176464 0.0944530
1987 2016 19516726.6 3e+06 4176464 0.0890403

Number of failed periods 1; 97% percent success

How are the results distributed?

plot_ending_bal_distribution(window_df, NULL, window_width)

Worst Period

library(png)
worst_period <- which.min(window_df$wdr)

worst_year_path <- paste0(output_path, 
                          window_df$start_year[worst_period], "-",
                          window_df$end_year[worst_period], ".png")
pp <- readPNG(worst_year_path, native = TRUE, info = TRUE)
plot(0:1, 0:1, type="n", ann=FALSE, axes=FALSE)
rasterImage(pp, 0, 0, 1, 1)

Best Period

best_period  <- which.max(window_df$ending_bal)

best_year_path <- paste0(output_path, 
                          window_df$start_year[best_period], "-",
                          window_df$end_year[best_period], ".png")
pp <- readPNG(best_year_path, native = TRUE, info = TRUE)
plot(0:1, 0:1, type="n", ann=FALSE, axes=FALSE)
rasterImage(pp, 0, 0, 1, 1)

Inflation

library(rvest)

inflation_table <- read_html("http://www.usinflationcalculator.com/inflation/historical-inflation-rates/") %>%
    html_node("table") %>%
  html_table(header = TRUE)

for (col in names(inflation_table)) {
    inflation_table[, col] <- as.numeric(inflation_table[, col])
}
    

Average annual inflation rate since 1914: 3.27%

Environment

devtools::session_info()
#> Session info --------------------------------------------------------------
#>  setting  value                       
#>  version  R version 3.3.0 (2016-05-03)
#>  system   x86_64, linux-gnu           
#>  ui       X11                         
#>  language en_US                       
#>  collate  en_US.UTF-8                 
#>  tz       posixrules                  
#>  date     2016-05-09
#> Packages ------------------------------------------------------------------
#>  package            * version  date       source        
#>  assertthat           0.1      2013-12-06 CRAN (R 3.3.0)
#>  bitops               1.0-6    2013-08-17 CRAN (R 3.3.0)
#>  colorspace           1.2-6    2015-03-11 CRAN (R 3.3.0)
#>  curl                 0.9.7    2016-04-10 CRAN (R 3.3.0)
#>  DBI                  0.4      2016-05-02 CRAN (R 3.3.0)
#>  devtools             1.11.1   2016-04-21 CRAN (R 3.3.0)
#>  digest               0.6.9    2016-01-08 CRAN (R 3.3.0)
#>  dplyr                0.4.3    2015-09-01 CRAN (R 3.3.0)
#>  evaluate             0.9      2016-04-29 CRAN (R 3.3.0)
#>  FinCal               0.6.2    2015-12-30 CRAN (R 3.3.0)
#>  formatR              1.3      2016-03-05 CRAN (R 3.3.0)
#>  ggplot2              2.1.0    2016-03-01 CRAN (R 3.3.0)
#>  gtable               0.2.0    2016-02-26 CRAN (R 3.3.0)
#>  highr                0.5.1    2015-09-18 CRAN (R 3.3.0)
#>  htmltools            0.3.5    2016-03-21 CRAN (R 3.3.0)
#>  httr                 1.1.0    2016-01-28 CRAN (R 3.3.0)
#>  knitr                1.12.3   2016-01-22 CRAN (R 3.3.0)
#>  lazyeval             0.1.10   2015-01-02 CRAN (R 3.3.0)
#>  lubridate            1.5.6    2016-04-06 CRAN (R 3.3.0)
#>  magrittr           * 1.5      2014-11-22 CRAN (R 3.3.0)
#>  memoise              1.0.0    2016-01-29 CRAN (R 3.3.0)
#>  munsell              0.4.3    2016-02-13 CRAN (R 3.3.0)
#>  plyr                 1.8.3    2015-06-12 CRAN (R 3.3.0)
#>  png                * 0.1-7    2013-12-03 CRAN (R 3.3.0)
#>  R6                   2.1.2    2016-01-26 CRAN (R 3.3.0)
#>  Rcpp                 0.12.4   2016-03-26 CRAN (R 3.3.0)
#>  RCurl                1.95-4.8 2016-03-01 CRAN (R 3.3.0)
#>  reshape2             1.4.1    2014-12-06 CRAN (R 3.3.0)
#>  rmarkdown            0.9.6    2016-05-01 CRAN (R 3.3.0)
#>  rvest              * 0.3.1    2015-11-11 CRAN (R 3.3.0)
#>  scales               0.4.0    2016-02-26 CRAN (R 3.3.0)
#>  selectr              0.2-3    2014-12-24 CRAN (R 3.3.0)
#>  sp500SlidingWindow * 0.1.0    2016-05-09 local         
#>  stringi              1.0-1    2015-10-22 CRAN (R 3.3.0)
#>  stringr              1.0.0    2015-04-30 CRAN (R 3.3.0)
#>  withr                1.0.1    2016-02-04 CRAN (R 3.3.0)
#>  XML                  3.98-1.4 2016-03-01 CRAN (R 3.3.0)
#>  xml2               * 0.1.2    2015-09-01 CRAN (R 3.3.0)
#>  yaml                 2.1.13   2014-06-12 CRAN (R 3.3.0)