library(tidyverse)
library(tidyquant)
library(Quandl)
library(lubridate)
library(ggrepel)
library(httr)
library(scales)
library(kableExtra)

Quandl.api_key(read_file("quandl.key"))

Goal

The goal is to evaluate an investment strategy suggested by an investment advisor in February 2016. The strategy plans to invest in 20 Swiss stocks and ETFs with a focus on dividend payout (dividend strategy). The constituents are equal weighted (5%) and rule based rebalancing takes place monthly. Dividends are not reinvested.

symbolsEUR <- c("MUV2.DE",
                "ALV.DE",
                "BAS.DE",
                "C029.F") # SPI ETF

symbols_namesEUR <- c("Muenchner Rueck",
                      "Allianz",
                      "BASF",
                      "SPI ETF")

symbolsCHF <- c("ROG.SW",
                "NESN.SW",
                "SGSN.SW",
                "SCMN.SW",
                "ZURN.SW",
                "CFR.SW",
                "BCVN.SW",
                "WARN.SW",
                "SPSN.SW",
                "CLN.SW",
                "BAER.SW",
                "ORON.SW",
                "CMBN.SW",
                "0DZH.L") # SMI ETF

symbols_namesCHF <- c("Roche",
                      "Nestle",
                      "SGS",
                      "Swisscom",
                      "Zurich Insur",
                      "Richemont",
                      "Banque Cant Vaud",
                      "Warteck Invest",
                      "Swiss Prime Site",
                      "Clariant",
                      "Julius Baer",
                      "Orior",
                      "Cembra",
                      "SMI ETF")

pricesCHF <- tq_get(symbolsCHF, from = "2016-01-01") %>% mutate(close_chf = close)
pricesEUR <- tq_get(symbolsEUR, from = "2016-01-01")

dividendsCHF <- tq_get(symbolsCHF, from = "2016-01-01", get = "dividends") %>% mutate(dividends_chf = dividends)
dividendsEUR <- tq_get(symbolsEUR, from = "2016-01-01", get = "dividends")

# VALOR: 1065278 / ISIN: IE0008471009
# https://www.finanzen.ch/etf/historisch/ishares-ii-plc---ishares-core-euro-stoxx-50-ucits-etf-eur-dist/swx/1.1.2015_6.6.2019
pricesfinanzenCHF <- read_csv("data/iShares_EURO_STOXX_50_CHF.csv") %>% 
  mutate(symbol = "iSharesSTOXXDiv", date = dmy(Datum)) %>% 
  select(symbol, date, 
         open = Eröffnung, high = Tageshoch, low = Tagestief, close = Tagestief, 
         volume = `Umsatz (St.)`) %>% 
  filter(date >= "2016-01-01") %>% 
  mutate(close_chf = close)

dividendsfinanzenCHF <- tibble(symbol = "iSharesSTOXXDiv",
                               date = date(c("2016-12-30", "2017-12-29", "2018-12-31", "2019-05-31")),
                               dividends = c(1.23, 1.14, 1.29, 0.49)) %>% 
  mutate(dividends_chf = dividends)

# VALOR: 42746037 / ISIN: LU1812092168
# https://www.finanzen.ch/etf/historisch/lyxor-stoxx-europe-select-dividend-30-ucits-etf---dist/fse/1.1.2015_6.6.2019
pricesfinanzenEUR <- read_csv("data/Lyxor_STOXX_Europe_Select_Dividend_EUR.csv") %>% 
  mutate(symbol = "LyxorSTOXXSelDiv", date = dmy(Datum)) %>% 
  select(symbol, date, 
         open = Eröffnung, high = Tageshoch, low = Tagestief, close = Tagestief, 
         volume = `Umsatz (St.)`) %>% 
  filter(date >= "2016-01-01")

dividendsfinanzenEUR <- tibble(symbol = "LyxorSTOXXSelDiv",
                               date = date(c("2016-12-30", "2017-12-29", "2018-12-31", "2019-05-31")),
                               dividends = c(0.83, 0.76, 0.81, 0))

eurchf <- Quandl("ECB/EURCHF", start_date="2016-01-01")

pricesEUR_CHF <- pricesEUR %>% 
  left_join(eurchf %>% rename(eurchf = Value), by = c("date" = "Date")) %>% 
  mutate(close_chf = close*eurchf)

pricesfinanzenEUR_CHF <- pricesfinanzenEUR %>% 
  left_join(eurchf %>% rename(eurchf = Value), by = c("date" = "Date")) %>% 
  mutate(close_chf = close*eurchf)

dividendsEUR_CHF <- dividendsEUR %>% 
  left_join(eurchf %>% rename(eurchf = Value), by = c("date" = "Date")) %>% 
  mutate(dividends_chf = dividends*eurchf)

dividendsfinanzenEUR_CHF <- dividendsfinanzenEUR %>% 
  left_join(eurchf %>% rename(eurchf = Value), by = c("date" = "Date")) %>% 
  mutate(dividends_chf = dividends*eurchf)

# putting it all togethet
prices <- bind_rows(pricesCHF, pricesEUR_CHF, pricesfinanzenCHF, pricesfinanzenEUR_CHF) %>% 
  arrange(symbol, date) %>% 
  group_by(symbol) %>%
  mutate(close_chf = na.locf(close_chf)) %>% 
  ungroup()

dividends <- bind_rows(dividendsCHF, dividendsEUR_CHF, dividendsfinanzenCHF, dividendsfinanzenEUR_CHF)

names_lut <- tibble(symbol = symbolsEUR, names = symbols_namesEUR) %>% 
  bind_rows(tibble(symbol = symbolsCHF, names = symbols_namesCHF)) %>% 
  bind_rows(tibble(symbol = "LyxorSTOXXSelDiv", names = "STOXX SelDiv ETF")) %>% 
  bind_rows(tibble(symbol = "iSharesSTOXXDiv", names = "STOXX Div ETF"))

prices %>% saveRDS("data/prices.RDS")
dividends %>% saveRDS("data/dividends.RDS")
names_lut %>% saveRDS("data/names_lut.RDS")

Performance analysis

prices <- readRDS("data/prices.RDS")
dividends <- readRDS("data/dividends.RDS")
names_lut <- readRDS("data/names_lut.RDS")

Check that we have 20 titles and all dividends. Actually we only need monthly (end-of-month) returns.

# # how many stocks do we have?
# prices %>% group_by(symbol) %>% count() %>% ungroup() %>% count()

# # did all pay dividends?
# dividends %>% group_by(symbol) %>% count() %>% ungroup() %>% count()

# #which is the one that did not pay?
# prices %>% select(symbol) %>% anti_join(dividends) %>% unique()
# # 0DZH.L is xtrackers SMI
price_returns <- prices %>% 
  group_by(symbol) %>%
  tq_transmute(select     = close_chf, 
               mutate_fun = periodReturn, 
               period     = "monthly", 
               col_rename = "Ra") %>%
  mutate(date = ceiling_date(date, "month")-1) %>% 
  ungroup()

price_returns %>% 
  left_join(names_lut, by = "symbol") %>% 
  ggplot(aes(y = Ra, x = reorder(names, desc(names)), color = names)) +
  geom_hline(yintercept = 0, color = "red") +
  geom_boxplot(color = "black", alpha = 0.5) +
  geom_point() +
  scale_y_continuous(labels = scales::percent) +
  coord_flip() +
  labs(title = "Price returns of selected stocks/ETFs", subtitle = "Jan 2016 - June 2019, monthly returns, arithmetic, excl. dividends",
       x = "Stock", y = "return",
       caption = "Data: Yahoo Finance, graph: mgei.github.io") +
  theme_tq() +
  theme(legend.position = "none")

Portfolio

We want to have a equal weightes portfolio. That’s 5% in each title.

weights = rep(1/20, 20)

portfolio_returns <- price_returns %>% filter(date > date("2016-03-01")) %>% 
  tq_portfolio(assets_col  = symbol, 
               returns_col = Ra, 
               weights     = weights, 
               col_rename  = "Ra",
               rebalance_on = "months")

portfolio_returns %>% mutate(symbol = "Portfolio") %>% 
  ggplot(aes(y = Ra, x = reorder(symbol, desc(symbol)), color = symbol)) +
  geom_hline(yintercept = 0, color = "red") +
  geom_boxplot(color = "black", alpha = 0.5) +
  geom_point() +
  scale_y_continuous(labels = scales::percent) +
  coord_flip() +
  labs(title = "Price returns on equal weightes stocks/ETFs portfolio", subtitle = "Jan 2016 - June 2019, monthly returns, arithmetic, excl. dividends",
       x = "", y = "return",
       caption = "Data: Yahoo Finance, graph: mgei.github.io") +
  theme_tq() +
  theme(legend.position = "none")

How did the portfolio grow over time? Need wealth index. Also we want data for the SMI and SPI for comparison. This is available from SIX.

# SMI
url <- "https://www.six-group.com/exchanges/downloads/indexdata/hsmi.csv"
GET(url, write_disk("data/temp.csv", overwrite = T))
data <- read_csv2("data/temp.csv", skip = 2)

smi <- data[-1:-4,] %>% 
  rename(Date = "SYMBOL") %>% 
  mutate(Date = as.Date(Date, "%d.%m.%Y")) %>% 
  mutate_if(is.character, funs(as.numeric(.))) %>% 
  select(date = Date, SMI)

# SPI
url <- "https://www.six-group.com/exchanges/downloads/indexdata/hspipr.csv"
GET(url, write_disk("data/temp.csv", overwrite = T))
data <- read_csv2("data/temp.csv", skip = 2) 

spi <- data[-1:-4,] %>% 
  rename(Date = "SYMBOL") %>% 
  mutate(Date = as.Date(Date, "%d.%m.%Y")) %>% 
  mutate_if(is.character, funs(as.numeric(.))) %>% 
  select(date = Date, SPI = SPIX)

benchmark <- bind_rows(smi %>% mutate(symbol = "SMI") %>% select(symbol, date, close_chf = SMI),
                       spi %>% mutate(symbol = "SPI") %>% select(symbol, date, close_chf = SPI))

benchmark %>% saveRDS("data/benchmark.RDS")
benchmark <- readRDS("data/benchmark.RDS")

benchmark_returns <- benchmark %>% 
  filter(year(date) >= 2016) %>% 
  group_by(symbol) %>%
  tq_transmute(select     = close_chf, 
               mutate_fun = periodReturn, 
               period     = "monthly", 
               col_rename = "Ra") %>%
  mutate(date = ceiling_date(date, "month")-1)

SMI_cumret <- benchmark_returns %>% filter(date > date("2016-03-01")) %>% 
  tq_portfolio(assets_col  = symbol, 
               returns_col = Ra, 
               weights     = c(1,0), 
               col_rename  = "wealth",
               rebalance_on = "months",
               wealth.index = T)

SPI_cumret <- benchmark_returns %>% filter(date > date("2016-03-01")) %>% 
  tq_portfolio(assets_col  = symbol, 
               returns_col = Ra, 
               weights     = c(0,1), 
               col_rename  = "wealth",
               rebalance_on = "months",
               wealth.index = T)
portfolio_value <- price_returns %>% filter(date > date("2016-03-01")) %>% 
  tq_portfolio(assets_col  = symbol, 
               returns_col = Ra, 
               weights     = weights, 
               col_rename  = "wealth",
               rebalance_on = "months",
               wealth.index = T)
  
bind_rows(portfolio_value %>% mutate(return = "Portfolio"),
          SMI_cumret %>% mutate(return = "SMI"),
          SPI_cumret %>% mutate(return = "SPI")) %>% 
  bind_rows(tibble(date = date("2016-03-01"), wealth = 1, return = c("Portfolio", "SMI", "SPI"))) %>% 
  ggplot(aes(x = date, y = wealth-1)) +
  geom_line(aes(color = return, size = if_else(return == "Portfolio", 2, 1))) +
  geom_hline(yintercept = 0, color = "red") +
  geom_hline(yintercept = max(portfolio_value["wealth"])-1, color = "red") +
  scale_x_date(date_breaks = "3 months", date_labels = "%b %y") +
  scale_y_continuous(labels = scales::percent, breaks = seq(0,1,0.025)) +
  scale_size_continuous(range = c(0.5, 2), guide = F) +
  labs(title = "Portfolio performance over time",
       subtitle = "Jan 2016 - June 2019, monthly returns, arithmetic, excl. dividends",
       x = "", y = "Cumulative return", color = "",
       caption = "Data: Yahoo Finance, graph: mgei.github.io") +
  theme_tq() +
  theme(axis.text.x = element_text(angle = 90))

Risk return plot

price_returns %>% filter(date > date("2016-03-01")) %>%
  left_join(names_lut) %>% mutate(symbol = names) %>% 
  bind_rows(portfolio_returns %>% mutate(symbol = "Portfolio")) %>% 
  bind_rows(benchmark_returns %>% filter(date > date("2016-03-01"))) %>% 
  group_by(symbol) %>% 
  summarise(mu = (1+mean(Ra))^12-1, sigma = sd(Ra)/sqrt(1/12)) %>% 
  ggplot(aes(x = sigma, y = mu, color = symbol, size = factor(symbol %in% c("Portfolio", "SMI", "SPI")))) +
  geom_point(alpha = 0.7) +
  geom_text_repel(aes(label = symbol)) +
  scale_size_discrete(range = c(3,7)) +
  scale_x_continuous(labels = scales::percent, breaks = seq(0,1, by = 0.025)) +
  scale_y_continuous(labels = scales::percent, breaks = seq(0,1, by = 0.025)) +
  labs(title = "Risk return diagram", subtitle = "Mar 2016 - June 2019, annualized values, portfolio equal weighted and monthly rebalanced",
       x = "Risk (Volatility, sigma)", y = "Return (mu)",
       caption = "Data: Yahoo Finance, graph: mgei.github.io") +
  theme_tq() +
  theme(legend.position = "none")

Dividends

For the dividends we need the number of stocks for each month and title. As defined above we have an equal weight portfolio with monthly rebalancement. The amount invested is 10 Mio. Swiss Francs.

total_portfolio <- 10000000 # 1 million CHF
total_per_title = total_portfolio/(prices %>% group_by(symbol) %>% count() %>% ungroup() %>% count() %>% pull())

shares <- prices %>% 
  group_by(symbol, year(date), month(date)) %>% 
  filter(date == max(date)) %>% 
  ungroup() %>% 
  select(symbol, date, close_chf) %>% 
  mutate(date = ceiling_date(date, "month") - 1) %>% 
  mutate(shares = total_per_title/close_chf)

dividends_paid <- dividends %>% 
  mutate(date = ceiling_date(date, "month") - 1) %>% 
  group_by(symbol, date) %>% 
  summarise(dividends_chf = sum(dividends_chf)) %>% 
  ungroup() %>% 
  left_join(shares, by = c("symbol", "date")) %>% 
  mutate(dividend_payout = dividends_chf*shares) %>% 
  group_by(symbol, year = year(date)) %>% 
  summarise(dividend_payout = sum(dividend_payout)) %>% 
  ungroup() %>% 
  mutate(dividend_return = dividend_payout/total_per_title)

dividends_paid %>% 
  left_join(names_lut) %>% mutate(symbol = names) %>% 
  ggplot(aes(x = year, y = reorder(symbol, desc(symbol)))) + 
  geom_point(aes(size = dividend_payout, color = symbol)) +
  geom_label(aes(label = number(dividend_payout, big.mark = "'")), hjust = 0, nudge_x = 0.1) +
  geom_label(aes(label = scales::percent(dividend_return, accuracy = 0.1)), hjust = 0, nudge_x = 0.45, size = 3) +
  scale_x_continuous(limits = c(2016, 2019.6)) +
  scale_size_continuous(range = c(0, 10)) +
  labs(title = "Dividends", subtitle = "Portfolio",
       x = "", y = "",
       caption = "Data: Yahoo Finance, graph: mgei.github.io") +
  theme_tq() +
  theme(legend.position = "none")

Overall perspective

How did we perform? We shall take into account:

Initial sum invested 10 million CHF.

initial_sum <- 10000000
flatfee_rate <- 0.004
income_tax_rate <- 0.27
wealth_tax_rate <- 0.008

dividends_paid_yearly <- dividends_paid %>% 
  group_by(year) %>% 
  summarise(dividend_payout = sum(dividend_payout))

table_data <- tibble(portfolio_value = initial_sum, year = 2015) %>% 
  bind_rows(portfolio_value %>% 
              mutate(portfolio_value = wealth*initial_sum) %>% 
              group_by(year = year(date)) %>% 
              filter(date == max(date)) %>% 
              ungroup()) %>% 
  left_join(dividends_paid_yearly, by = "year") %>% 
  mutate(fee = if_else(year>2015, portfolio_value*flatfee_rate, NA_real_),
         income_tax = dividend_payout*income_tax_rate,
         wealth_tax = if_else(year>2015, portfolio_value*wealth_tax_rate, NA_real_),
         cash_in_pocket = dividend_payout - fee - income_tax - wealth_tax,
         change_in_portfolio_value = portfolio_value-lag(portfolio_value),
         total_performance = cash_in_pocket+change_in_portfolio_value) %>% 
  select("Year" = year, "Portfolio value" = portfolio_value, 
         "Total dividends paid" = dividend_payout,
         "Bank/broker fee" = fee,
         "Income tax" = income_tax,
         "Wealth tax" = wealth_tax,
         "Cash in hand after fee and tax" = cash_in_pocket,
         "Change in portfolio value" = change_in_portfolio_value,
         "Total performance after fee and tax" = total_performance) %>% 
  mutate(Year = as.character(Year) %>% str_replace("2015", "initial"))

options(knitr.kable.NA = "")

table_data %>% 
  kable(format.args = list(big.mark = "'"), digits = 0) %>% 
  kable_styling(bootstrap_options = c("striped", "hover", "condensed")) %>% 
  row_spec(0, bold = T)
Year Portfolio value Total dividends paid Bank/broker fee Income tax Wealth tax Cash in hand after fee and tax Change in portfolio value Total performance after fee and tax
initial 10’000’000
2016 10’901’952 380’758 43’608 102’805 87’216 147’130 901’952 1’049’082
2017 12’896’404 288’629 51’586 77’930 103’171 55’943 1’994’453 2’050’395
2018 11’194’768 354’308 44’779 95’663 89’558 124’308 -1’701’636 -1’577’329
2019 12’328’921 303’933 49’316 82’062 98’631 73’924 1’134’153 1’208’077

Is it worth it?