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

• We will explore common errors when calculating returns - particularly when using simple returns.

• We will explore how to do return calculations safely using xts and PerformanceAnalytics (it is less safe than you thought).

• We will end with a simple workflow example of doing portfolio return calculations safely using xts –> PerformanceAnalytics

### 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:

• When doing statistical analyses - you should prefer the asymptotically normal dlog returns.

• When doing actual portfolio analytics - you should be using simple returns (i.e. what you would actually earn if you invested in an asset).

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 <-
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:


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

##### 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:

• Same weights:

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 )   TRUE • Same Returns:  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)
)

 TRUE
• Also note that this is quite different from multiplying all the returns with the start dates:

all.equal(
# Portfolio Returns using the safe function:

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  0.02898551$Safe_PA
 0.02898551

\$Not_Safe_PA
 -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

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