Aggregate portfolio contributions through time

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, including asset contributions for each period:
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.quarters$BOP.Weight["2014"]
#            SPY.Adjusted AGG.Adjusted
# 2014-01-31    0.6000000    0.4000000
# 2014-02-28    0.5876652    0.4123348
# 2014-03-31    0.5975060    0.4024940
# 2014-04-30    0.6000000    0.4000000
# 2014-05-30    0.5996912    0.4003088
# 2014-06-30    0.6023973    0.3976027
# 2014-07-31    0.6000000    0.4000000
# 2014-08-29    0.5973447    0.4026553
# 2014-09-30    0.6039059    0.3960941
# 2014-10-15    0.6000000    0.4000000

# Look at monthly contribution from each asset
result.quarters$contribution["2014"]
#            SPY.Adjusted  AGG.Adjusted
# 2014-01-31 -0.021147406  0.0061514949
# 2014-02-28  0.026753095  0.0015515892
# 2014-03-31  0.004943173 -0.0006035523
# 2014-04-30  0.004178138  0.0033039234
# 2014-05-30  0.013920140  0.0046954857
# 2014-06-30  0.012434880 -0.0002195083
# 2014-07-31 -0.008069401 -0.0009942920
# 2014-08-29  0.023590440  0.0046081450
# 2014-09-30 -0.008343079 -0.0024215991
# 2014-10-15 -0.032250533  0.0067572530

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).

EDIT: Even knowing that, I got it wrong the first time… Based on the reference that Paolo points to in his comment below and some subsequent email conversation, I’ve replaced the last part of this post with the correct calculations.

From the portfolio contributions of individual assets, such as those of a particular asset class or manager, the multi-period contribution is neither the sum of nor the geometric compounding of single-period contributions. Because the weights of the individual assets change through time as transactions occur, the capital base for the asset changes.

Instead, the asset’s multi-period contribution is the sum of the asset’s dollar contributions from each period, as calculated from the wealth index of the total portfolio. Once contributions are expressed as a change in dollar value relative to the wealth index of the portfolio, asset contributions then sum to the returns of the total portfolio for the period.

# Calculate weighted contributions
# cumulative returns lagged forward to represent beginning of the period portfolio value
lag.cum.ret <- na.fill(lag(cumprod(1+result.quarters$returns),1),1) 
# multiply by contributions to get weighted contributions
wgt.contrib = result.quarters$contribution * rep(lag.cum.ret, NCOL(result.quarters$contribution))

# Create end of year dates for xts timestamps
dates = c(seq(as.Date("2003/12/31"), tail(index(returns),1), "years"), tail(index(returns),1))

# Summarize weighted contributions by year
ann.wgt.contrib = apply(wgt.contrib, 2, function (x) apply.yearly(x, sum))
ann.wgt.contrib = as.xts(ann.wgt.contrib, order.by=dates)

# Normalize to the beginning of period value
p.ann.contrib = NULL
for(i in 2003:2014) 
  p.ann.contrib = rbind(p.ann.contrib, colSums(wgt.contrib[as.character(i)]/rep(head(lag.cum.ret[as.character(i)],1),NCOL(wgt.contrib))))
p.ann.contrib = as.xts(p.ann.contrib, order.by = dates)
p.ann.contrib = cbind(p.ann.contrib, rowSums(p.ann.contrib))
colnames(p.ann.contrib) = c("SPY Contrib", "AGG Contrib", "Portfolio Return")
p.ann.contrib
#            SPY Contrib  AGG Contrib Portfolio Return
# 2003-12-31  0.07116488  0.001458576       0.07262346
# 2004-12-31  0.06465335  0.015395509       0.08004886
# 2005-12-31  0.02927809  0.009055321       0.03833341
# 2006-12-31  0.09375945  0.016132091       0.10989154
# 2007-12-31  0.03113908  0.027212530       0.05835161
# 2008-12-31 -0.23405576  0.028503181      -0.20555258
# 2009-12-31  0.15850172  0.011712674       0.17021440
# 2010-12-31  0.09597257  0.025305730       0.12127830
# 2011-12-31  0.01689928  0.031037641       0.04793692
# 2012-12-31  0.09585370  0.015927463       0.11178116
# 2013-12-31  0.18533901 -0.008329840       0.17700917
# 2014-08-29  0.03670684  0.019700427       0.05640727

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?

# Calculate the annual return of the portfolio for each year between 2003 
# and current YTD
> period.apply(result.quarters$returns, INDEX=endpoints(result.quarters$returns, "years"), FUN=Return.cumulative, geometric=TRUE)
#            portfolio.returns
# 2003-12-31        0.07262346
# 2004-12-31        0.08004886
# 2005-12-30        0.03833341
# 2006-12-29        0.10989154
# 2007-12-31        0.05835161
# 2008-12-31       -0.20555258
# 2009-12-31        0.17021440
# 2010-12-31        0.12127830
# 2011-12-30        0.04793692
# 2012-12-31        0.11178116
# 2013-12-31        0.17700917
# 2014-10-15        0.03793038 
# Yes, the results match!

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.

Knowing that, I went ahead and drafted a function for aggregating contributions called to.period.contributions that’s in the sandbox on R-Forge. Once you’ve sourced the function into your environment, you can aggregate contributions as such:

to.period.contributions(result.quarters$contribution, "years")
#            SPY.Adjusted AGG.Adjusted Portfolio Return
# 2003-12-31   0.07116488  0.001458576       0.07262346
# 2004-12-31   0.06465335  0.015395509       0.08004886
# 2005-12-30   0.02927809  0.009055321       0.03833341
# 2006-12-29   0.09375945  0.016132091       0.10989154
# 2007-12-31   0.03113908  0.027212530       0.05835161
# 2008-12-31  -0.23405576  0.028503181      -0.20555258
# 2009-12-31   0.15850172  0.011712674       0.17021440
# 2010-12-31   0.09597257  0.025305730       0.12127830
# 2011-12-30   0.01689928  0.031037641       0.04793692
# 2012-12-31   0.09585370  0.015927463       0.11178116
# 2013-12-31   0.18533901 -0.008329840       0.17700917
# 2014-10-15   0.01455321  0.023377173       0.03793038

Along with that I created a few wrapper functions for to.weekly, to.monthly.contributions, to.quarterly.contributions, and to.yearly.contributions. Give those a shot and let me know if you see any issues. Thanks again to Paolo for the feedback!

Advertisements
Tagged ,

3 thoughts on “Aggregate portfolio contributions through time

  1. Paolo says:

    This is a very welcome enhancement. As you said aggregation of contributions across time period is not trivial as it may looks but even your formulas don’t get it right after careful checking your examples and vignette.

    I must admit that the mistake is not easy to spot since the sum of the multi-period contributions are equal to the multi-period portfolio return indeed (but that doesn’t mean they are correct per se) and your formulas are correct for the no-rebalancing case (only).

    You can find the correct formulas on page 36 of the “Total Portfolio Performance Attribution
    Methodology” by Morningstar – http://corporate.morningstar.com/US/documents/MethodologyDocuments/MethodologyPapers/TotalPortfolioPerformanceAttributionMethodology.pdf

    You can find some code below showing that…feel free to drop me an email if you want an helpful spreadsheet I put together. I can find some time early next week to polish it if it can help.

    ——-

    ############################
    ## test without rebalancing
    w <- xts(matrix(c(.2,.7,.1),1), as.Date("2014-04-29"))
    returns <- xts(cbind(c(2,-7,-3,-9,8),c(0,1,-1,1,1),c(11,-9,9,-4,7))/100,
    as.Date(c("2014-04-30","2014-05-30","2014-06-30","2014-07-31","2014-08-29")))
    names(w) = names(returns) = c("A","B","C")

    result <- Return.portfolio(returns, weights=w, verbose = T)

    # Calculate aggregated contribution by asset
    x.C = (coredata(last(result$EOP.Value)) – coredata(first(result$BOP.Value))) / (sum(first(result$BOP.Value)))
    rowSums(x.C)
    Return.cumulative(result$returns) #they match and…
    x.C # multi-period contributions are correct BUT…

    ########################
    ## test with rebalancing
    w <- xts(rbind(c(.2,.7,.1),c(.2,.7,.1)), as.Date(c("2014-04-29","2014-06-29")))
    names(w) = names(returns)

    result <- Return.portfolio(returns, weights=w, verbose = T)

    # Calculate aggregated contribution by asset
    x.C = (coredata(last(result$EOP.Value)) – coredata(first(result$BOP.Value))) / (sum(first(result$BOP.Value)))
    rowSums(x.C)
    Return.cumulative(result$returns) #they match but…
    x.C # multi-period contributions are wrong

    #correct solution by using Morningstar's formulas
    adj.cum.ret <- na.fill(lag(cumprod(1+result$returns),1),1)
    multi.period.contrib <- rep(adj.cum.ret,3) * result$BOP.Weight * returns
    sum(colSums(multi.period.contrib))
    Return.cumulative(result$returns) #they match and…
    colSums(multi.period.contrib) # multi-period contributions are correct

    • Peter Carl says:

      Thank you very much for the pointer – I’ve corrected the relevant parts above and written a function to capture it. Let me know if you see anything else… pcc

  2. Peter says:

    Thank you very much, quick question about irregular rebalancing. If I have, say 10 years of daily data and 5 assets, where I specify rebalancing on arbitrary dates (for example 3 times a year), what weights are assumed in the interim periods between the specified rebalances? Does it revert to equal-weights, or does is use “drifting” weights based on the asset returns since the last rebalance? thank you very much!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: