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!