Portfolio Performance since 2 April 18
- Include Google Sheet iframe link in future.
An all weather portfolio
Note: I carried out a similar analysis as 1 of the posts I’ve written (https://jirong-huang.netlify.com/post/perm_port/). The difference is that I considered 4 assets (including T-bills) instead of 3. The performance in the post is more reflective of stress-test perfromance (Sharpe, Drawdown Ratio, etc.)
Based on the literature, permanent portfolio is an investment strategy that is able to yield moderate returns and relatively low volatility. Investor is recommended to invest equally (25%) into GLD, Index, Bond and Cash and rebalance it back to this proportion on regular intervals.
This approach is something that I’m keen to adopt for a portion of my portfolio.
To investigate the feasibility, I simulated a portfolio starting at $400 on 2009 (limited by complete series). Here’s the asset allocation proportion,
- GLD: Gold
- TLT: 20 Years Treasury Bond
- VGSH: Vanguard Short Term Bond
- VTI: Total US stock
Note:
- I admit that the code that I wrote here is very procedural. If I’ve time, I’ll try to ‘functionalize’ the codes so that I could use it for other portfolio simulation purposes.
- The looping in the R vectorized setting is known to be super slow! If I really have time to spare, I’ll convert some of the loops to Rcpp (C++). But I highly doubt so since the data is not that huge at the moment (unless I venture into tick data. Oh well, who knows).
Key points
On the annualized returns, it’s not really fantastic. But it still returns a respectable annualized performance of 5% (+ 2% dividends?)
What stands out, however, are the following performance metrics:
- In terms of draw-down, it performed remarkably well relative to a Total US Stock portfolio. It suffered a loss of only 9% as compared to a huge loss of 20 + % in other assets; could be more if full Great Financial Crisis is included.
- On the Sharpe Ratio (annualized returns in excess of risk-free rate per unit of volatility), it’s considerably higher than other assets - albeit lower than Total Stock (in my other analysis with longer time period, it’s considerably higher).
Feel free to adapt or adopt the code. You can easily substitute the stocks based on your preferred asset allocation.
Setting up the analysis.
In this section, I downloaded stock data using quant mod. And merge the time series.
library(quantmod)
## Loading required package: xts
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
## Loading required package: TTR
## Version 0.4-0 included new data defaults. See ?getSymbols.
library(ggplot2)
library(PerformanceAnalytics)
##
## Attaching package: 'PerformanceAnalytics'
## The following object is masked from 'package:graphics':
##
## legend
library(tidyr)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:xts':
##
## first, last
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tibble)
# Download data-->Fix the ending date for project section
ticker1 = "GLD"
stock1 = getSymbols(ticker1,from="1900-01-01",auto.assign=F)
## 'getSymbols' currently uses auto.assign=TRUE by default, but will
## use auto.assign=FALSE in 0.5-0. You will still be able to use
## 'loadSymbols' to automatically load data. getOption("getSymbols.env")
## and getOption("getSymbols.auto.assign") will still be checked for
## alternate defaults.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.warning4.0"=FALSE). See ?getSymbols for details.
##
## WARNING: There have been significant changes to Yahoo Finance data.
## Please see the Warning section of '?getSymbols.yahoo' for details.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.yahoo.warning"=FALSE).
names(stock1) = c("open","high","low","close","volume","adj_close")
stock1 = stock1[,6]
ticker2 = "TLT"
stock2 = getSymbols(ticker2,from="1900-01-01",auto.assign=F)
names(stock2) = c("open","high","low","close","volume","adj_close")
stock2 = stock2[,6]
ticker3 = "VGSH"
stock3 = getSymbols(ticker3,from="1900-01-01",auto.assign=F)
names(stock3) = c("open","high","low","close","volume","adj_close")
stock3 = stock3[,6]
ticker4 = "VTI"
stock4 = getSymbols(ticker4,from="1900-01-01",auto.assign=F)
names(stock4) = c("open","high","low","close","volume","adj_close")
stock4 = stock4[,6]
# Merge the time series and subset NA
#These are the various time series
ticker_list = c("stock1","stock2","stock3","stock4")
#read in list. Loop through and assign variable to holder variable. Then assign it to combined list
ticker_all = get(ticker_list[1])
#Merging in the time series
for(i in 2:length(ticker_list)){
ticker_ind = get(ticker_list[i])
ticker_all = merge(ticker_all,ticker_ind)
}
names(ticker_all) = c("stock1","stock2","stock3","stock4")
ticker_all = subset(ticker_all,!is.na(ticker_all$stock1) & !is.na(ticker_all$stock2) & !is.na(ticker_all$stock3) & !is.na(ticker_all$stock4))
Running the simulation
Next, I run the simulation - By intialising 25% asset allocation in each of the asset and rebalance at the end of each year. Daily portfolio returns are then obtained throught the ROC function.
#Assign equal weights to each stream of returns
ticker_all = cbind(ticker_all,rowMeans(ticker_all))
ticker_all = subset(ticker_all,!is.na(ticker_all[,4]))
ticker_all = ticker_all[,-ncol(ticker_all)]
#Identify the period of rebalancing. Show the indexes
rebal_index = data.frame(index = endpoints(ticker_all,on="years")[-1])
# endpoints(ticker_all,on="quarters")
#Merge in the indicator into ticker_all--Can't seem to merge. Will do the inefficient loop
# merge(ticker_all,rebal_index, by = "index", all = T)
ticker_all$rebal = NA
for(i in 1:nrow(rebal_index)){
ticker_all$rebal[rebal_index$index[i]] = 1
}
ticker_all$rebal = ifelse(is.na(ticker_all$rebal),0,ticker_all$rebal)
#Create the returns for each price series
ticker_all$ret1 = ROC(ticker_all[,1])
ticker_all$ret2 = ROC(ticker_all[,2])
ticker_all$ret3 = ROC(ticker_all[,3])
ticker_all$ret4 = ROC(ticker_all[,4])
#Initialise value for each stock series, with a total portfolio value
ticker_all$val1 = NA; ticker_all$val1[1] = 100
ticker_all$val2 = NA; ticker_all$val2[1] = 100
ticker_all$val3 = NA; ticker_all$val3[1] = 100
ticker_all$val4 = NA; ticker_all$val4[1] = 100
ticker_all$portfolio_val = NA
ticker_all$portfolio_val[1] = rowSums(ticker_all[1,which(names(ticker_all) == "val1"):which(names(ticker_all) == "val4")])
#Loop each row and 'compound'. Till it reaches the rebalancing date. Then reset stock value amount in that day. Take the portfolio value in t-1
for(i in 2:nrow(ticker_all)){
if(as.numeric(ticker_all$rebal[i]) == 0){
#During non-rebalancing days
ticker_all$val1[i] = as.numeric(ticker_all$val1[i-1]) * (1 + as.numeric(ticker_all$ret1[i]))
ticker_all$val2[i] = as.numeric(ticker_all$val2[i-1]) * (1 + as.numeric(ticker_all$ret2[i]))
ticker_all$val3[i] = as.numeric(ticker_all$val3[i-1]) * (1 + as.numeric(ticker_all$ret3[i]))
ticker_all$val4[i] = as.numeric(ticker_all$val4[i-1]) * (1 + as.numeric(ticker_all$ret4[i]))
ticker_all$portfolio_val[i] = rowSums(ticker_all[i,which(names(ticker_all) == "val1"):which(names(ticker_all) == "val4")])
}else{
#During re-balancing days
ticker_all$val1[i] = ticker_all$portfolio_val[i-1] / 4
ticker_all$val2[i] = ticker_all$portfolio_val[i-1] / 4
ticker_all$val3[i] = ticker_all$portfolio_val[i-1] / 4
ticker_all$val4[i] = ticker_all$portfolio_val[i-1] / 4
ticker_all$portfolio_val[i] = rowSums(ticker_all[i,which(names(ticker_all) == "val1"):which(names(ticker_all) == "val4")])
}
}
#Generate the daily portfolio returns
ticker_all$portfolio_ret = ROC(ticker_all$portfolio_val)
Portfolio Performance for the entire period
On the annualized returns, it’s not really fantastic. But it still returns a respectable annualized performance of 5% (+ 2% dividends?)
What stands out, however, are the following performance metrics:
- In terms of draw-down, it performed remarkably well relative to a GOLD & Total US Stock portfolio. It suffered a loss of only 9% as compared to a loss of 20+ % in other assets; could’ve be more if full Great Financial Crisis is included (in my post analysis, it could go up to 60% if Great Financial Crisis is included).
- On the Sharpe Ratio (annualized returns in excess of risk-free rate per unit of volatility), it’s considerably higher than other assets - albeit lower than Total Stock (in my other analysis with longer time period, it’s considerably higher).
######################################Study the portfolio returns########################################
#Carry out the portfolio return series
table.Drawdowns(ticker_all$portfolio_ret, top=10)
## From Trough To Depth Length To Trough Recovery
## 1 2012-10-05 2013-06-26 2014-06-19 -0.0882 427 180 247
## 2 2016-07-11 2016-12-15 2017-08-30 -0.0828 289 112 177
## 3 2015-01-26 2016-01-14 2016-06-08 -0.0804 346 246 100
## 4 2009-12-03 2010-02-10 2010-04-14 -0.0471 90 47 43
## 5 2018-01-29 2018-02-08 <NA> -0.0415 46 9 NA
## 6 2011-09-07 2011-09-28 2011-11-03 -0.0409 42 16 26
## 7 2012-02-29 2012-05-16 2012-08-24 -0.0391 125 55 70
## 8 2011-11-08 2011-12-15 2012-01-27 -0.0368 55 27 28
## 9 2010-11-09 2010-11-17 2011-02-28 -0.0317 76 7 69
## 10 2014-09-02 2014-09-17 2014-12-09 -0.0274 70 12 58
table.DownsideRisk(merge(ticker_all$portfolio_ret, ticker_all$ret1, ticker_all$ret2, ticker_all$ret3, ticker_all$ret4))
## portfolio_ret ret1 ret2 ret3
## Semi Deviation 0.0029 0.0077 0.0065 0.0005
## Gain Deviation 0.0023 0.0066 0.0056 0.0004
## Loss Deviation 0.0028 0.0079 0.0060 0.0004
## Downside Deviation (MAR=210%) 0.0090 0.0128 0.0117 0.0083
## Downside Deviation (Rf=0%) 0.0028 0.0076 0.0064 0.0004
## Downside Deviation (0%) 0.0028 0.0076 0.0064 0.0004
## Maximum Drawdown 0.0882 0.4922 0.2147 0.0106
## Historical VaR (95%) -0.0062 -0.0169 -0.0146 -0.0010
## Historical ES (95%) -0.0090 -0.0253 -0.0193 -0.0014
## Modified VaR (95%) -0.0064 -0.0177 -0.0146 -0.0010
## Modified ES (95%) -0.0108 -0.0342 -0.0210 -0.0015
## ret4
## Semi Deviation 0.0071
## Gain Deviation 0.0062
## Loss Deviation 0.0077
## Downside Deviation (MAR=210%) 0.0119
## Downside Deviation (Rf=0%) 0.0069
## Downside Deviation (0%) 0.0069
## Maximum Drawdown 0.2169
## Historical VaR (95%) -0.0158
## Historical ES (95%) -0.0237
## Modified VaR (95%) -0.0158
## Modified ES (95%) -0.0294
table.AnnualizedReturns(merge(ticker_all$portfolio_ret, ticker_all$ret1, ticker_all$ret2, ticker_all$ret3, ticker_all$ret4))
## portfolio_ret ret1 ret2 ret3 ret4
## Annualized Return 0.0469 -0.0017 0.0500 0.0069 0.1206
## Annualized Std Dev 0.0611 0.1651 0.1427 0.0101 0.1519
## Annualized Sharpe (Rf=0%) 0.7674 -0.0103 0.3503 0.6756 0.7937
charts.PerformanceSummary(merge(ticker_all$portfolio_ret, ticker_all$ret1, ticker_all$ret2, ticker_all$ret3, ticker_all$ret4))
Yearly Portfolio Performance
I also tabulated the yearly metrics of the portfolio to give a sense of the performance over years.
#####################################Study the annualized portfolio returns series#######################
#Use the rebal-indicator. Loop through the chunks
# https://www.quantmod.com/documentation/periodReturn.html
# https://rpubs.com/mohammadshadan/288218
# yearly_ret = periodReturn(ticker_all$portfolio_val
# ,period='yearly',subset='2004::') # returns years 2003 to present
yearly_ret = periodReturn(ticker_all$portfolio_val
,period='yearly') # returns years 2003 to present
#Inefficient way to calulate standard deviation. If it've time, I will probably optimize this
split_val = split(ticker_all$portfolio_ret, f = "years")
yearly_ret$annual_sd = sapply(X = split_val, FUN = StdDev) * sqrt(252)
getSymbols('DGS3MO',src = 'FRED')
## [1] "DGS3MO"
rf = DGS3MO; rm(DGS3MO)
rf = rf["2009/2018"]
split_val_rf = split(rf$DGS3MO, f = "years")
yearly_ret$annual_rf = sapply(X = split_val_rf, FUN = mean, na.rm = T)/100
yearly_ret$Sharpe = (yearly_ret$yearly.returns - yearly_ret$annual_rf)/yearly_ret$annual_sd
yearly_ret
## yearly.returns annual_sd annual_rf Sharpe
## 2009-12-31 -0.02143548 0.07391424 0.0015092000 -0.3104230
## 2010-12-31 0.12503869 0.06925292 0.0013844622 1.7855452
## 2011-12-30 0.09201080 0.07228724 0.0005284000 1.2655400
## 2012-12-31 0.05353343 0.05504939 0.0008760000 0.9565489
## 2013-12-31 -0.02919578 0.06965068 0.0005708000 -0.4273696
## 2014-12-31 0.09449067 0.04879063 0.0003272000 1.9299499
## 2015-12-31 -0.03608942 0.06101808 0.0005250996 -0.6000601
## 2016-12-30 0.05106338 0.06221028 0.0031936000 0.7694834
## 2017-12-29 0.10340429 0.04264321 0.0094896000 2.2023362
## 2018-04-03 -0.01057814 0.06051025 0.0158516129 -0.4367815
plot(yearly_ret$yearly.returns)
plot(yearly_ret$Sharpe)