Setup: Load Data

library(tidyverse)
library(lubridate)
library(knitr)

# Read the ETF price data
etf <- read.csv("myetf4.csv", stringsAsFactors = FALSE)
colnames(etf) <- c("Date", "X0050", "X0056", "X006205", "X00646")
etf$Date <- as.Date(etf$Date, format = "%Y/%m/%d")

# Filter insample: 2015/12/14 to 2018/12/28
insample <- etf %>%
  filter(Date >= as.Date("2015-12-14") & Date <= as.Date("2018-12-28")) %>%
  arrange(Date)

cat("Insample period:", format(min(insample$Date)), "to", format(max(insample$Date)), "\n")
## Insample period: 2015-12-14 to 2018-12-28
cat("Number of observations:", nrow(insample), "\n")
## Number of observations: 751

Q1. GMVP Using Daily Returns

Compute Daily Returns

prices_daily <- insample[, c("X0050", "X0056", "X006205", "X00646")]

# Log daily returns
ret_daily <- as.matrix(log(prices_daily[-1, ] / prices_daily[-nrow(prices_daily), ]))
colnames(ret_daily) <- c("0050", "0056", "006205", "00646")

cat("Daily return matrix dimensions:", dim(ret_daily), "\n")
## Daily return matrix dimensions: 750 4
cat("\nSample mean daily returns:\n")
## 
## Sample mean daily returns:
print(round(colMeans(ret_daily) * 100, 4))
##    0050    0056  006205   00646 
##  0.0424  0.0362 -0.0277  0.0226

GMVP: Daily Returns

The Global Minimum Variance Portfolio (GMVP) minimizes portfolio variance:

\[\min_{w} \; w^\top \Sigma w \quad \text{subject to} \quad \mathbf{1}^\top w = 1\]

The closed-form solution is:

\[w^* = \frac{\Sigma^{-1} \mathbf{1}}{\mathbf{1}^\top \Sigma^{-1} \mathbf{1}}\]

# Covariance matrix of daily returns
Sigma_d <- cov(ret_daily)
ones <- rep(1, 4)
Sigma_d_inv <- solve(Sigma_d)

# GMVP weights
w_gmvp_d <- (Sigma_d_inv %*% ones) / as.numeric(t(ones) %*% Sigma_d_inv %*% ones)
w_gmvp_d <- as.vector(w_gmvp_d)
names(w_gmvp_d) <- colnames(ret_daily)

cat("=== GMVP Weights (Daily Returns) ===\n")
## === GMVP Weights (Daily Returns) ===
kable(data.frame(
  ETF       = names(w_gmvp_d),
  Weight    = round(w_gmvp_d, 6),
  Weight_pct = paste0(round(w_gmvp_d * 100, 4), "%")
), col.names = c("ETF", "Weight", "Weight (%)"), align = "lrr")
ETF Weight Weight (%)
0050 0050 -0.224131 -22.4131%
0056 0056 0.729873 72.9873%
006205 006205 0.108076 10.8076%
00646 00646 0.386183 38.6183%

GMVP Performance (Daily)

gmvp_d_ret <- as.numeric(t(w_gmvp_d) %*% colMeans(ret_daily))
gmvp_d_var <- as.numeric(t(w_gmvp_d) %*% Sigma_d %*% w_gmvp_d)
gmvp_d_sd  <- sqrt(gmvp_d_var)

cat(sprintf("Daily Return  : %.6f  (%.4f%%)\n", gmvp_d_ret, gmvp_d_ret * 100))
## Daily Return  : 0.000226  (0.0226%)
cat(sprintf("Daily Std Dev : %.6f  (%.4f%%)\n", gmvp_d_sd,  gmvp_d_sd  * 100))
## Daily Std Dev : 0.005922  (0.5922%)
kable(data.frame(
  Metric = c("Daily Return", "Daily Std Dev"),
  Value  = c(round(gmvp_d_ret, 6), round(gmvp_d_sd, 6)),
  Pct    = c(paste0(round(gmvp_d_ret * 100, 4), "%"),
             paste0(round(gmvp_d_sd  * 100, 4), "%"))
), col.names = c("Metric", "Value", "Value (%)"), align = "lrr")
Metric Value Value (%)
Daily Return 0.000226 0.0226%
Daily Std Dev 0.005922 0.5922%

Q2. GMVP Using Monthly Returns

Compute Monthly Returns

# Keep last trading day of each month
monthly <- insample %>%
  mutate(YM = format(Date, "%Y-%m")) %>%
  group_by(YM) %>%
  slice_tail(n = 1) %>%
  ungroup() %>%
  arrange(YM)

prices_m <- monthly[, c("X0050", "X0056", "X006205", "X00646")]

# Monthly log returns
ret_monthly <- as.matrix(log(prices_m[-1, ] / prices_m[-nrow(prices_m), ]))
colnames(ret_monthly) <- c("0050", "0056", "006205", "00646")

cat("Monthly return matrix dimensions:", dim(ret_monthly), "\n")
## Monthly return matrix dimensions: 36 4
cat("\nSample mean monthly returns:\n")
## 
## Sample mean monthly returns:
print(round(colMeans(ret_monthly) * 100, 4))
##    0050    0056  006205   00646 
##  0.8206  0.6625 -0.6612  0.4075

GMVP: Monthly Returns

Sigma_m     <- cov(ret_monthly)
Sigma_m_inv <- solve(Sigma_m)

w_gmvp_m <- (Sigma_m_inv %*% ones) / as.numeric(t(ones) %*% Sigma_m_inv %*% ones)
w_gmvp_m <- as.vector(w_gmvp_m)
names(w_gmvp_m) <- colnames(ret_monthly)

cat("=== GMVP Weights (Monthly Returns) ===\n")
## === GMVP Weights (Monthly Returns) ===
kable(data.frame(
  ETF        = names(w_gmvp_m),
  Weight     = round(w_gmvp_m, 6),
  Weight_pct = paste0(round(w_gmvp_m * 100, 4), "%")
), col.names = c("ETF", "Weight", "Weight (%)"), align = "lrr")
ETF Weight Weight (%)
0050 0050 -0.028541 -2.8541%
0056 0056 0.520687 52.0687%
006205 006205 -0.001783 -0.1783%
00646 00646 0.509637 50.9637%

GMVP Performance (Monthly)

gmvp_m_ret <- as.numeric(t(w_gmvp_m) %*% colMeans(ret_monthly))
gmvp_m_var <- as.numeric(t(w_gmvp_m) %*% Sigma_m %*% w_gmvp_m)
gmvp_m_sd  <- sqrt(gmvp_m_var)

cat(sprintf("Monthly Return  : %.6f  (%.4f%%)\n", gmvp_m_ret, gmvp_m_ret * 100))
## Monthly Return  : 0.005304  (0.5304%)
cat(sprintf("Monthly Std Dev : %.6f  (%.4f%%)\n", gmvp_m_sd,  gmvp_m_sd  * 100))
## Monthly Std Dev : 0.025069  (2.5069%)
kable(data.frame(
  Metric = c("Monthly Return", "Monthly Std Dev"),
  Value  = c(round(gmvp_m_ret, 6), round(gmvp_m_sd, 6)),
  Pct    = c(paste0(round(gmvp_m_ret * 100, 4), "%"),
             paste0(round(gmvp_m_sd  * 100, 4), "%"))
), col.names = c("Metric", "Value", "Value (%)"), align = "lrr")
Metric Value Value (%)
Monthly Return 0.005304 0.5304%
Monthly Std Dev 0.025069 2.5069%

Comparison: Daily vs Monthly GMVP Weights

kable(data.frame(
  ETF       = c("0050", "0056", "006205", "00646"),
  Daily_w   = round(w_gmvp_d, 6),
  Monthly_w = round(w_gmvp_m, 6)
), col.names = c("ETF", "Weight (Daily)", "Weight (Monthly)"), align = "lrr")
ETF Weight (Daily) Weight (Monthly)
0050 0050 -0.224131 -0.028541
0056 0056 0.729873 0.520687
006205 006205 0.108076 -0.001783
00646 00646 0.386183 0.509637

Q3. Tangency Portfolio (Monthly Returns, Rf = 0)

The Tangency Portfolio maximizes the Sharpe Ratio. With \(R_f = 0\):

\[w_{TP} = \frac{\Sigma^{-1} \mu}{\mathbf{1}^\top \Sigma^{-1} \mu}\]

where \(\mu\) is the vector of mean returns (since \(R_f = 0\), excess returns equal raw returns).

mu_m <- colMeans(ret_monthly)

num_tp <- Sigma_m_inv %*% mu_m
w_tp   <- num_tp / as.numeric(t(ones) %*% num_tp)
w_tp   <- as.vector(w_tp)
names(w_tp) <- colnames(ret_monthly)

cat("=== Tangency Portfolio Weights (Monthly, Rf = 0) ===\n")
## === Tangency Portfolio Weights (Monthly, Rf = 0) ===
kable(data.frame(
  ETF        = names(w_tp),
  Weight     = round(w_tp, 6),
  Weight_pct = paste0(round(w_tp * 100, 4), "%")
), col.names = c("ETF", "Weight", "Weight (%)"), align = "lrr")
ETF Weight Weight (%)
0050 0050 1.278969 127.8969%
0056 0056 -0.087371 -8.7371%
006205 006205 -0.896758 -89.6758%
00646 00646 0.705159 70.5159%

Tangency Portfolio Performance

tp_ret <- as.numeric(t(w_tp) %*% mu_m)
tp_var <- as.numeric(t(w_tp) %*% Sigma_m %*% w_tp)
tp_sd  <- sqrt(tp_var)
tp_sr  <- tp_ret / tp_sd   # Rf = 0

cat(sprintf("Monthly Return  : %.6f  (%.4f%%)\n", tp_ret, tp_ret * 100))
## Monthly Return  : 0.018719  (1.8719%)
cat(sprintf("Monthly Std Dev : %.6f  (%.4f%%)\n", tp_sd,  tp_sd  * 100))
## Monthly Std Dev : 0.047094  (4.7094%)
cat(sprintf("Sharpe Ratio    : %.6f\n", tp_sr))
## Sharpe Ratio    : 0.397475
kable(data.frame(
  Metric = c("Monthly Return", "Monthly Std Dev", "Sharpe Ratio (Rf=0)"),
  Value  = c(round(tp_ret, 6), round(tp_sd, 6), round(tp_sr, 6))
), col.names = c("Metric", "Value"), align = "lr")
Metric Value
Monthly Return 0.018719
Monthly Std Dev 0.047094
Sharpe Ratio (Rf=0) 0.397475

Summary: All Three Portfolios

kable(data.frame(
  Portfolio  = c("GMVP (Daily)", "GMVP (Monthly)", "Tangency (Monthly)"),
  w_0050     = round(c(w_gmvp_d["0050"],   w_gmvp_m["0050"],   w_tp["0050"]),   4),
  w_0056     = round(c(w_gmvp_d["0056"],   w_gmvp_m["0056"],   w_tp["0056"]),   4),
  w_006205   = round(c(w_gmvp_d["006205"], w_gmvp_m["006205"], w_tp["006205"]), 4),
  w_00646    = round(c(w_gmvp_d["00646"],  w_gmvp_m["00646"],  w_tp["00646"]),  4),
  Return_pct = c(paste0(round(gmvp_d_ret * 100, 4), "%"),
                 paste0(round(gmvp_m_ret * 100, 4), "%"),
                 paste0(round(tp_ret     * 100, 4), "%")),
  StdDev_pct = c(paste0(round(gmvp_d_sd * 100, 4), "%"),
                 paste0(round(gmvp_m_sd * 100, 4), "%"),
                 paste0(round(tp_sd     * 100, 4), "%")),
  Sharpe     = c(round(gmvp_d_ret / gmvp_d_sd, 4),
                 round(gmvp_m_ret / gmvp_m_sd, 4),
                 round(tp_sr, 4))
),
col.names = c("Portfolio", "w(0050)", "w(0056)", "w(006205)", "w(00646)",
              "Return", "Std Dev", "Sharpe"),
align = "lrrrrrrr",
caption = "Summary of GMVP and Tangency Portfolio Results")
Summary of GMVP and Tangency Portfolio Results
Portfolio w(0050) w(0056) w(006205) w(00646) Return Std Dev Sharpe
GMVP (Daily) -0.2241 0.7299 0.1081 0.3862 0.0226% 0.5922% 0.0382
GMVP (Monthly) -0.0285 0.5207 -0.0018 0.5096 0.5304% 2.5069% 0.2116
Tangency (Monthly) 1.2790 -0.0874 -0.8968 0.7052 1.8719% 4.7094% 0.3975