Aggregate portfolio contributions through time
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
The last CRAN release didn’t have much new functionality, but Ross Bennett and I have completely re-written the Return.portfolio
function to fix some issues and make the calculations more transparent. The function calculates the returns of a portfolio given asset returns, weights, and rebalancing periods – which, although not rocket science, requires some diligence about it.
Users of this function frequently want to aggregate contribution through time – but contribution for higher periodicity data can’t be directly accumulated into lower periodicities (e.g., using daily contributions to calculate monthly contributions). So the function now also outputs values for the individual assets and the aggregated portfolio so that contributions can be calculated at different periodicities. For example, contribution during a quarter can be calculated as the change in value of the position through those three months, divided by the original value of the portfolio. The function doesn’t do this directly, but it provides the value calculation so that it can be done.
We’ve also added some other convenience features to that function. If you do not specify weights, the function assumes an equal weight portfolio. Alternatively, you can specify a vector or single-row matrix of weights that matches the length of the asset columns. In either case, if you don’t specify a rebalancing period, the weights will be applied at the beginning of the asset time series and no further rebalancing will take place. If a rebalancing period is specified (using the endpoints attribute of ‘days’, ‘weeks’, ‘months’, ‘quarters’, and ‘years’ from xts’ endpoints
function), the portfolio will be rebalanced to the given weights at the interval specified.
That function can also do irregular rebalancing when passed a time series of weights. It uses the date index of the weights for xts-style subsetting of rebalancing periods, and treats those weights as “end-of-period” weights (which seems to be the most common use case).
When verbose=TRUE
, Return.portfolio now returns a list of data and intermediary calculations. Those should allow anyone to step through the specific calculations and see exactly how the numbers are generated.
Ross did a very nice vignette for the function (vignette(portfolio_returns)
), and as usual there’s a lot more detail in the documentation – take a look.
Here’s an example of a traditional 60/40 portfolio. We’ll look at the results of different rebalancing period assumptions, and then aggregate the monthly portfolio contributions to yearly contributions.
library(quantmod) library(PerformanceAnalytics) symbols = c( "SPY", # US equities, SP500 "AGG" # US bonds, Barclay Agg ) getSymbols(symbols, from="1970-01-01") x.P <- do.call(merge, lapply(symbols, function(x) { Cl(to.monthly(Ad(get(x)), drop.time = TRUE, indexAt='endof')) })) colnames(x.P) = paste0(symbols, ".Adjusted") x.R <- na.omit(Return.calculate(x.P)) head(x.R) # SPY.Adjusted AGG.Adjusted # 2003-10-31 0.05350714 -0.009464182 # 2003-11-28 0.01095923 0.003380861 # 2003-12-31 0.05035552 0.009815412 # 2004-01-30 0.01975363 0.004352241 # 2004-02-27 0.01360322 0.011411238 # 2004-03-31 -0.01331329 0.006855184 tail(x.R) # SPY.Adjusted AGG.Adjusted # 2014-04-30 0.006931012 0.008151410 # 2014-05-30 0.023211141 0.011802974 # 2014-06-30 0.020650814 -0.000551116 # 2014-07-31 -0.013437564 -0.002481390 # 2014-08-29 0.039463463 0.011516492 # 2014-09-15 -0.008619401 -0.010747791
If we didn’t pass in any weights, the function would assume an equal-weight portfolio. We’ll specify a 60/40 split instead.
# Create a weights vector w = c(.6,.4) # Traditional 60/40 Equity/Bond portfolio weights # No rebalancing period specified, so buy and hold initial weights result.norebal = Return.portfolio(x.R, weights=w) table.AnnualizedReturns(result.norebal) # portfolio.returns # Annualized Return 0.0705 # Annualized Std Dev 0.0880 # Annualized Sharpe (Rf=0%) 0.8008
If we don’t specify a rebalancing period, we get buy and hold returns. Instead, let’s rebalance every year.
# Rebalance annually back to 60/40 proportion result.years = Return.portfolio(x.R, weights=w, rebalance_on="years") table.AnnualizedReturns(result.years) # portfolio.returns # Annualized Return 0.0738 # Annualized Std Dev 0.0861 # Annualized Sharpe (Rf=0%) 0.8565
Similarly, we might want to consider quarterly rebalancing. But this time we’ll collect all of the intermediary calculations, including position values. We get a list back this time.
# Rebalance quarterly; provide full calculations result.quarters = Return.portfolio(x.R, weights=w, rebalance_on="quarters", verbose=TRUE) table.AnnualizedReturns(result.quarters$returns) # portfolio.returns # Annualized Return 0.0723 # Annualized Std Dev 0.0875 # Annualized Sharpe (Rf=0%) 0.8254
That provides more detail, including the monthly contributions from each asset.
# We asked for a verbose result, so the function generates a list of # intermediary calculations: names(result.quarters) # [1] "returns" "contribution" "BOP.Weight" "EOP.Weight" # [5] "BOP.Value" "EOP.Value" # Examine the beginning-of-period weights; note the reweighting periods result$BOP.Weight["2014"] # SPY.Adjusted AGG.Adjusted # 2014-01-31 0.6000000 0.4000000 # 2014-02-28 0.5876714 0.4123286 # 2014-03-31 0.5974947 0.4025053 # 2014-04-30 0.6000000 0.4000000 # 2014-05-30 0.5997093 0.4002907 # 2014-06-30 0.6023978 0.3976022 # 2014-07-31 0.6000000 0.4000000 # 2014-08-29 0.5973465 0.4026535 # 2014-09-15 0.6038840 0.3961160 # Look at monthly contribution from each asset result.quarters$contribution["2014"] # SPY.Adjusted AGG.Adjusted # 2014-01-31 -0.021147541 0.0061409364 # 2014-02-28 0.026762266 0.0015876664 # 2014-03-31 0.004952419 -0.0006024964 # 2014-04-30 0.004158607 0.0032605640 # 2014-05-30 0.013919936 0.0047246212 # 2014-06-30 0.012440004 -0.0002191250 # 2014-07-31 -0.008062538 -0.0009925558 # 2014-08-29 0.023573361 0.0046371558 # 2014-09-15 -0.005205118 -0.0042573724
Having the monthly contributions is nice, but what if we want to know what each asset contributed to the annual result of the portfolio? We get this question quite a bit (and it has prompted many attempts to “fix” the code – we appreciate that isn’t as straightforward as it seems). Contributions aren’t summable through time, so we have to calculate the change in value of each asset in the portfolio. In the “verbose” mode, the Return.portfolio calculates asset values to make this easier.
# Get the EOY values to calculate yearly contribution for each asset x.eopV = do.call(merge, lapply(result.quarterly$EOP.Value, function(x) { Cl(to.yearly(x)) } )) colnames(x.eopV) = colnames(result.quarterly$BOP.Weight) # set the column names # Calculate the beginning of period value of the portfolio to use as the denominator x.bopV = rowSums(do.call(merge, lapply(result$BOP.Value, function(x) { Op(to.yearly(x)) } ))) # Calculate aggregated annual contribution by asset x.C = diff(x.eopV)/x.bopV x.C # SPY.Adjusted AGG.Adjusted # 2003-12-31 NA NA # 2004-12-31 0.042124364 0.03799342 # 2005-12-30 0.007630034 0.03061038 # 2006-12-29 0.076970818 0.03291215 # 2007-12-31 0.005853651 0.05256041 # 2008-12-31 -0.168975064 -0.03663254 # 2009-12-31 0.196991200 -0.02673838 # 2010-12-31 0.088499182 0.03279392 # 2011-12-30 0.025845816 0.02212378 # 2012-12-31 0.042697031 0.06905367 # 2013-12-31 0.134941865 0.04204158 # 2014-09-15 0.020898444 0.04562386
So that provides the annual contribution of each asset for each asset. Let’s check the result – do the annual contributions for each instrument sum to the portfolio returns for the year?
x.P = as.xts(rowSums(x.C), order.by=index(x.eopV)) colnames(x.P) = "Annual Portfolio Return" x.P # Annual Portfolio Return # 2003-12-31 NA # 2004-12-31 0.08011779 # 2005-12-30 0.03824042 # 2006-12-29 0.10988297 # 2007-12-31 0.05841406 # 2008-12-31 -0.20560760 # 2009-12-31 0.17025282 # 2010-12-31 0.12129310 # 2011-12-30 0.04796960 # 2012-12-31 0.11175070 # 2013-12-31 0.17698345 # 2014-09-15 0.06652230 # Yes, the results match: > for(i in 2004:2014) {print(as.numeric(Return.cumulative(result$returns[as.character(i)])))} # [1] 0.08011779 # [1] 0.03824042 # [1] 0.109883 # [1] 0.05841406 # [1] -0.2056076 # [1] 0.1702528 # [1] 0.1212931 # [1] 0.0479696 # [1] 0.1117507 # [1] 0.1769834 # [1] 0.0665223
So that’s an example of how one would go about aggregating return contributions from a higher periodicity (monthly) to a lower periodicity (yearly) within a portfolio. I hope that’s not too confusing, but don’t hesitate to let me know if you see a better way to do this.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.