How Portfolio Calculations can go wrong… Easily

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

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

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 <-
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()
```

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

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

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.

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

```
[1] 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)
)
```

```
[1] 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:
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"
```

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

For starters, it starts on the second day - i.e. it produces weights and returns from the day after the initial weights are provided.

E.g. from the vignette it states: “Rebalancing periods can be thought of as taking effect immediately after the close of the bar. So, a March 31 rebalancing date will actually be in effect for April 1.”

In most applications this does not fit with my own workflow.

Second (and most frustratingly) - the function is order dependent.

Thus it does not square stock X, Y and Z for R and weight inputs, but rather considers column orders.

This is extremely dangerous - and can easily cause unintended mistakes to enter your calculations.

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

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

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

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

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