Perils of Portfolio Returns

How Portfolio Calculations can go wrong… Easily

N.F. Katzke https://bio.nfkatzke.com
01-04-2019

Purpose

The aim of this blogpost is to detail the common perils when calculating portfolio returns.

Required packages

For the rest of the blog to work, ensure the following packages are installed by running the following first:


if(!require("tidyverse") ) library(tidyverse)
if(!require("lubridate") ) library(lubridate)
if(!require("tbl2xts") ) library(tbl2xts)
if(!require("rmsfuns") ) library(rmsfuns)

Simple Returns

The first question naturally when doing portfolio calculations is whether to use log- or simple returns.

While there are other sources that go into more detail on the distinction between the two methods of calculation, a good rule of thumb is:

For the remainder of this blog we will assume you are interested in simple returns.

For portfolio return calculations - it is important to remember that we are interested in quantities held and prices. Weights are simply derived from these.

Let’s load some data and follow with the calculations to see why this is important and failure to keep this in mind can cause you to make mistakes easily:


df <- 
  read_csv("https://raw.githubusercontent.com/Nicktz/ExDat/master/extdata/RetEx.csv", 
           col_types = cols(.default = "d", date = "D"))

Notice that the \(df\) is now given in positions and prices (weights can thus be calculated with these.) Let’s now tidy this up to make it easier to see what is going on here:


df_Adj <- 
left_join(
  
  df %>% gather(Company, Shares, ends_with("_Shares"))  %>% 
    select(date, Company, Shares) %>% mutate(Company = gsub("_Shares", "", Company)),
  
  df %>% gather(Company, Price, ends_with("_Price")) %>% 
    select(date, Company, Price) %>% mutate(Company = gsub("_Price", "", Company)),
  
  by = c("date", "Company")
)

Let’s now calculate the daily weights by hand to illustrate


By_Hand <- 
  df_Adj %>% 
  group_by(date, Company) %>% 
  mutate(Stock_Value_Held = Shares*Price) %>% 
  group_by(date) %>% 
  mutate(Port = sum(Shares*Price, na.rm=T)) %>% 
  mutate(weight = Stock_Value_Held / Port) %>% 
  arrange(date) %>% ungroup()

Weight Comparisons

A common error in calculating returns is to simply multiply the start weights with subsequent returns throughout. E.g.:

Given: starting weights (no rebalancing after) and returns

Asked: Calculate Portfolio Returns.

Wrong Way: Multiplying out weights with returns:

Wrong_Returns <- 

  left_join(
  
  Returns %>% gather(Company, Returns, -date),
  
  Weights %>% gather(Company, StartWeight, -date) %>% select(-date),
  
  by = "Company"
  
) %>% group_by(date) %>% 
  
  summarise(Wrong_Portfolio = sum(Returns*StartWeight, na.rm = T) )
Right Way: Adjusting weights for changes in Returns, then multiplying it with returns:

We should instead use the following formula each day to make the correct adjustment:

\[ w_{i,t} = \frac{w_{i, t-1}*(1+r_{i, t-1})}{\sum_j{w_{j, t-1}*(1+r_{j, t-1})}}, \quad \forall j \]

This can be quite a daunting task to do in a tidy environment (add the code in the comment if you want to attempt this).

My suggestion here would be to use PerformanceAnalytics’ Return.portfolio function, which does this at breakneck speed. We will below ensure it is the same as the calculation done by hand.

We will also be using the tbl2xts package here to move effortlessly between tbls and xts.

Using PerformanceAnalytics::Return.portfolio

The process to follow below is to first define the weights, and then the returns separately as xts.

Then plug it into PerformanceAnalytics::Return.portfolio. I will follow the illustration with a caveat to where this function can go wrong - and argue using instead a safer function for calculating returns (so please read to the end).


weights <- 
  By_Hand %>% filter(date == first(date)) %>% 
  select(date, Company, weight) %>% 
    tbl_xts(cols_to_xts = "weight", spread_by = "Company")

R <- 
  df_Adj %>% group_by(Company) %>% mutate(Ret = Price / lag(Price) - 1) %>% 
  tbl_xts(cols_to_xts = "Ret", spread_by = "Company")

# Safe return portfolio calc:
df_R <- 
  PerformanceAnalytics::Return.portfolio(R = R, weights = weights, geometric = TRUE, verbose = TRUE)

Now we can see that the weights and returns calculated by hand corresponds exactly to those calculated above:


all.equal(
  
  By_Hand %>% filter(date > first(date)) %>% 
    select(date, Company, weight) %>% 
    spread(Company, weight) %>% mutate_at(vars(-date), funs(round(., 10))),
  
  df_R$EOP.Weight %>% xts_tbl %>% mutate_at(vars(-date), funs(round(., 10))), 
  
  ignore_col_order = T
)

[1] TRUE

all.equal(
# Portfolio Returns using the safe function:
df_R$returns %>% xts_tbl() %>% pull(portfolio.returns),
# Portfolio Returns by hand
By_Hand %>% select(date, Port) %>% unique %>% mutate(Ret = Port / lag(Port) - 1) %>% 
  filter(date > first(date)) %>%  select(date, Ret) %>% pull(Ret)
)

[1] TRUE

all.equal(
# Portfolio Returns using the safe function:
df_R$returns %>% xts_tbl() %>% pull(portfolio.returns),
# Portfolio Returns by hand
Wrong_Returns %>% filter(date > first(date)) %>%  
  select(date, Wrong_Portfolio) %>% pull(Wrong_Portfolio)
)

[1] "Mean relative difference: 0.104094"

Caveat: making PerformanceAnalytics::Return.portfolio safe again

While the above calculations are useful, PerformanceAnalytics::Return.portfolio can toss up a few surprizes if you are not careful.

To solve both the above issues - instead use the safer wrapper from rmsfuns: Safe_Return.portfolio.


weights <- 
  By_Hand %>% filter(date == first(date)) %>% 
  select(date, Company, weight) %>% 
  tbl_xts(cols_to_xts = "weight", spread_by = "Company")

weights_reordered <- 
  By_Hand %>% filter(date == first(date)) %>% 
  select(date, Company, weight) %>% 
  tbl_xts(cols_to_xts = "weight", spread_by = "Company") %>% 
  # Let's reorder the names:
  .[,c(1,3,2)]

R <- 
  df_Adj %>% group_by(Company) %>% mutate(Ret = Price / lag(Price) - 1) %>% 
  tbl_xts(cols_to_xts = "Ret", spread_by = "Company")

# Safe return portfolio calc:
df_R_Correct <- 
  rmsfuns::Safe_Return.portfolio(R = R, weights = weights, 
                                 geometric = TRUE, lag_weights = TRUE, verbose = TRUE)

# The PA version is sensitive to the order of the columns...
df_R_Wrong <- 
  PerformanceAnalytics::Return.portfolio(R = R, weights = weights_reordered, 
                                         geometric = TRUE, verbose = TRUE)

Weights Comparison

Now we can see that the weights calculated by hand corresponds exactly to the Safe Portfolio Return Weights (EOP):


bind_rows(

  tail(By_Hand %>% select(date, Company, weight) %>% spread(Company, weight) %>% 
         mutate_at(vars(-date), funs(round(., 10))), 1) %>% mutate(Which = "By_Hand"),
  
  df_R_Correct$EOP.Weight %>% tail(1) %>% xts_tbl %>% mutate(Which = "Safe_PA"),

  tail(df_R_Wrong$EOP.Weight %>% xts_tbl %>% mutate_at(vars(-date), funs(round(., 10))), 1) %>% mutate(Which = "Not_safe_PA")
)

# A tibble: 3 x 5
  date           A     C     X Which      
  <date>     <dbl> <dbl> <dbl> <chr>      
1 2018-01-20 0.503 0.127 0.370 By_Hand    
2 2018-01-20 0.503 0.127 0.370 Safe_PA    
3 2018-01-20 0.537 0.229 0.235 Not_safe_PA

Cumulative Return Comparison

Of course, the different weights will also cause completely different portfolio returns:


c(
  "ByHand" = 
    df_R_Correct$returns %>% xts_tbl %>% 
    # mutate(Cum = cumprod(1+portfolio.returns)) %>% 
    mutate(Cum = cumprod(1+portfolio.returns) - 1) %>% 
    select(Cum) %>% 
    tail(1),
    "Safe_PA" = PerformanceAnalytics::Return.cumulative(df_R_Correct$returns, geometric = T),
    "Not_Safe_PA" = PerformanceAnalytics::Return.cumulative(df_R_Wrong$returns, geometric = T)
)

$ByHand.Cum
[1] 0.02898551

$Safe_PA
[1] 0.02898551

$Not_Safe_PA
[1] -0.03588532

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Katzke (2019, Jan. 4). Curious Quant Blog: Perils of Portfolio Returns. Retrieved from https://curiousquant.com/posts/2019-01-04-perils-of-portfolio-returns/

BibTeX citation

@misc{katzke2019perils,
  author = {Katzke, N.F.},
  title = {Curious Quant Blog: Perils of Portfolio Returns},
  url = {https://curiousquant.com/posts/2019-01-04-perils-of-portfolio-returns/},
  year = {2019}
}