library(tidyverse)
library(lubridate)
library(scales)
library(ggrepel)
library(tidyquant)
library(jsonlite)
theme_set(theme_minimal())
invisible(Sys.setlocale("LC_TIME", "en_US.UTF-8"))

Imagine we are a Swiss investor. We look at several stock market indices. For all of these we use the Total Return index (DAX is already a performance index, for the SPI we need specifically the SPI TR, and for the S&P 500 we use the TR version).

library(familyoffice)

spi <- fo_get("SXGE", from = as.Date("2015-01-01"))
spx <- tq_get("^SP500TR", from = as.Date("2015-01-01"))
dax <- tq_get("^GDAXI", from = as.Date("2015-01-01"))

indices <- bind_rows(spi |> transmute(index = "SPI TR", currency = "CHF", date, close = close),
                     spx |> transmute(index = "S&P 500 TR", currency = "USD", date,close = close),
                     dax |> transmute(index = "DAX", currency = "EUR", date,close = close))

indices |> saveRDS("data/currencyhedging/indices.RDS")

fx <- tq_get(c("EURCHF=X", "USDCHF=X"), from = as.Date("2015-01-01"))

fx <- fx |> 
  transmute(currency = str_remove(symbol, "CHF=X"), date, rate = close)

fx |> saveRDS("data/currencyhedging/fx.RDS")
indices <- readRDS("data/currencyhedging/indices.RDS")
fx <- readRDS("data/currencyhedging/fx.RDS")
indices |> 
  group_by(index) |>
  mutate(p = close / first(close)-1) |> 
  ggplot(aes(date, p, color = index)) +
  geom_line() +
  scale_y_continuous(labels = scales::percent, breaks = 0:50/5) +
  labs(title = "Performance of Stock Market Indices",
       subtitle = "Total Return Indices (each in its own currency)",
       x = NULL,
       y = "Performance",
       color = NULL) +
  theme(legend.position = "bottom")

S&P almost gaines 240% in 10 years, DAX 110%, and SPI only 80%. What a poor performance for Swiss stocks.

But wait a minute. We are a Swiss investor. We have to convert the foreign indices into Swiss Francs. We can do this by multiplying the performance of the index with the exchange rate.

indices_fx <- indices |> 
  left_join(fx, by = c("date", "currency")) |>
  group_by(index) |> 
  mutate(rate = na.locf0(rate)) |>
  ungroup() |> 
  mutate(rate = ifelse(currency == "CHF", 1, rate)) |> 
  mutate(close_chf = close * rate)

indices_fx |>
  group_by(index) |>
  mutate(p_chf = close_chf / first(close_chf)-1) |>
  ggplot(aes(date, p_chf, color = index)) +
  geom_line() +
  scale_y_continuous(labels = scales::percent, breaks = 0:50/5) +
  labs(title = "Performance of Stock Market Indices",
       subtitle = "Total Return Indices (converted to CHF)",
       x = NULL,
       y = "Performance",
       color = NULL) +
  theme(legend.position = "bottom")

In Swiss francs, the S&P 500 gained 210%, the SPI 80%, and the DAX just 60%. As the Swiss franc strengthened against the Euro and the US Dollar, the performance of the DAX and the S&P 500 was reduced.

Now should you currency hedge? There are ETF products tracking the indices with a built in currency hedge. Let’s compare the performance of the hedged and the unhedged ETFs.

spx_hedged <- fo_get("IE00B88DZ566", from = as.Date("2015-01-01"))
spx_unhedged <- fo_get("IE00B3YCGJ38", from = as.Date("2015-01-01"), currency = "USD")

etfs <- bind_rows(spx_hedged |> transmute(etf = "iShares S&P 500 CHF Hedged", date, close = close, currency = "CHF"),
                  spx_unhedged |> transmute(etf = "Invesco S&P 500", date, close = close, currency = "USD"))

etfs |> saveRDS("data/currencyhedging/etfs.RDS")
etfs <- readRDS("data/currencyhedging/etfs.RDS")
etfs_fx <- etfs |> 
  left_join(fx, by = c("date", "currency")) |>
  group_by(etf) |> 
  mutate(rate = na.locf0(rate)) |>
  ungroup() |> 
  mutate(rate = ifelse(currency == "CHF", 1, rate)) |> 
  mutate(close_chf = close * rate)

etfs_fx |>
  group_by(etf) |>
  mutate(p_chf = close_chf / first(close_chf)-1) |>
  ggplot(aes(date, p_chf, color = etf)) +
  geom_line() +
  scale_y_continuous(labels = scales::percent, breaks = 0:50/5) +
  labs(title = "Performance of ETFs",
       subtitle = "S&P 500 ETFs (converted to CHF)",
       x = NULL,
       y = "Performance",
       color = NULL) +
  theme(legend.position = "bottom")

Hedged underperformed.

What about past two years?

p1 <- etfs_fx |>
  filter(date >= as.Date("2023-01-01")) |>
  group_by(etf) |>
  mutate(p_chf = close_chf / first(close_chf)-1) |>
  ggplot(aes(date, p_chf, color = etf)) +
  geom_line() +
  scale_y_continuous(labels = scales::percent, breaks = 0:50/5) +
  labs(title = "Performance of ETFs",
       subtitle = "S&P 500 ETFs (converted to CHF)",
       x = NULL,
       y = "Performance",
       color = NULL) +
  theme(legend.position = "bottom")

p1

Here the hedged ETF was almost equal to the unhedged ETF up to Q3 2024. From then on, the USD gained against the CHF. It then made sense that the unhedged ETF outperformed the hedged ETF.

Here’s the corresponding currency rate added to the plot.

p2 <- fx |> 
  filter(date >= as.Date("2023-01-01")) |>
  filter(currency == "USD") |>
  ggplot(aes(date, rate)) +
  geom_line() +
  labs(subtitle = "USD/CHF Exchange Rate",
       x = NULL,
       y = "Rate")

library(patchwork)

arrangeplots <- "
1
1
1
2"
p1 / p2 + plot_layout(design = arrangeplots)

As the USD gains, the hedged ETF underperforms and vice versa. But not by as much as one would imagine.

Since 2023…

Why did the hedged ETF do so poor?

Could it be because of the fees? Not really, the ETF has a moderate TER of 0.2% p.a.

It’s most likely the cost of currency hedging. The ETF has to pay for the currency forward contracts. The cost of hedging is the difference between the interest rates of the two currencies. And as the interest rates has been relatively high in USD, the cost of hedging was high.

Hedging currency risks can also introduce additional costs and complexity without consistently improving returns (as we’ve just seen). Fluctuations tend to balance out. As one currency loses, it’s equities often gain overproportionally (not true in extreme cases), and vice versa.