Aggregate portfolio contributions through time

September 25, 2014
By

(This article was first published on tradeblotter » R, and kindly contributed to R-bloggers)

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.

To leave a comment for the author, please follow the link and comment on their blog: tradeblotter » R.

R-bloggers.com offers daily e-mail updates about R news and tutorials on topics such as: Data science, Big Data, R jobs, visualization (ggplot2, Boxplots, maps, animation), programming (RStudio, Sweave, LaTeX, SQL, Eclipse, git, hadoop, Web Scraping) statistics (regression, PCA, time series, trading) and more...



If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.

Search R-bloggers


Sponsors

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)