Background

Assets: 0050 · 0056 · 006205 · 00646 (Taiwan ETFs)
In-sample period: 2015/12/14 – 2018/12/28
Goals: Find the GMVP using daily returns (Q1), monthly returns (Q2), and the Tangency Portfolio under Rf = 0 (Q3).


Data Preparation

library(tidyverse)
library(knitr)
library(kableExtra)

# Load data
df <- read.csv("myetf4.csv", stringsAsFactors = FALSE)
df$Index <- as.Date(df$Index)
colnames(df) <- c("Date", "X0050", "X0056", "X006205", "X00646")
rownames(df) <- NULL

# Filter in-sample period
insample <- df %>% filter(Date >= "2015-12-14" & Date <= "2018-12-28")
cat("In-sample observations:", nrow(insample), "\n")
In-sample observations: 751 
cat("Date range:", format(min(insample$Date)), "to", format(max(insample$Date)), "\n")
Date range: 2015-12-14 to 2018-12-28 

Q1 — GMVP: Daily Returns

Step 1 · Compute Daily Returns

prices_d <- insample %>% select(X0050, X0056, X006205, X00646)

# Daily log or simple returns — we use simple pct change
daily_ret <- data.frame(
  X0050   = diff(prices_d$X0050)   / head(prices_d$X0050,   -1),
  X0056   = diff(prices_d$X0056)   / head(prices_d$X0056,   -1),
  X006205 = diff(prices_d$X006205) / head(prices_d$X006205, -1),
  X00646  = diff(prices_d$X00646)  / head(prices_d$X00646,  -1)
)
cat("Daily return observations:", nrow(daily_ret), "\n")
Daily return observations: 750 
# Mean daily returns
mu_d <- colMeans(daily_ret)
cat("\nMean Daily Returns:\n")

Mean Daily Returns:
print(round(mu_d, 6))
    X0050     X0056   X006205    X00646 
 0.000463  0.000385 -0.000212  0.000255 
# Covariance matrix
cov_d <- cov(daily_ret)
cat("\nCovariance Matrix (Daily):\n")

Covariance Matrix (Daily):
print(round(cov_d, 8))
            X0050     X0056    X006205    X00646
X0050   7.837e-05 4.559e-05 0.00004467 3.663e-05
X0056   4.559e-05 4.526e-05 0.00002674 2.354e-05
X006205 4.467e-05 2.674e-05 0.00013042 2.910e-05
X00646  3.663e-05 2.354e-05 0.00002910 5.903e-05

Step 2 · Analytical GMVP Weights

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

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

The closed-form solution is:

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

n <- 4
ones <- rep(1, n)
cov_inv_d <- solve(cov_d)

# GMVP weights
w_gmvp_d <- (cov_inv_d %*% ones) / as.numeric(t(ones) %*% cov_inv_d %*% ones)
names(w_gmvp_d) <- c("0050", "0056", "006205", "00646")

cat("=== GMVP Weights (Daily Returns) ===\n")
=== GMVP Weights (Daily Returns) ===
print(round(w_gmvp_d, 6))
             [,1]
X0050   -0.219358
X0056    0.728372
X006205  0.107623
X00646   0.383363
attr(,"names")
[1] "0050"   "0056"   "006205" "00646" 
cat("\nSum of weights:", round(sum(w_gmvp_d), 6), "\n")

Sum of weights: 1 

Step 3 · Portfolio Return & Risk

ret_gmvp_d  <- as.numeric(t(w_gmvp_d) %*% mu_d)
var_gmvp_d  <- as.numeric(t(w_gmvp_d) %*% cov_d %*% w_gmvp_d)
std_gmvp_d  <- sqrt(var_gmvp_d)

results_q1 <- data.frame(
  Metric = c("Optimal Weights — 0050", "Optimal Weights — 0056",
             "Optimal Weights — 006205", "Optimal Weights — 00646",
             "Portfolio Daily Return", "Portfolio Daily Std Dev"),
  Value  = c(round(w_gmvp_d, 6), round(ret_gmvp_d, 6), round(std_gmvp_d, 6))
)

kable(results_q1, caption = "Q1: GMVP Results (Daily Returns)", align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Q1: GMVP Results (Daily Returns)
Metric Value
Optimal Weights — 0050 -0.219358
Optimal Weights — 0056 0.728372
Optimal Weights — 006205 0.107623
Optimal Weights — 00646 0.383363
Portfolio Daily Return 0.000254
Portfolio Daily Std Dev 0.005905

Note: The analytical GMVP allows short-selling. 0050 receives a small negative weight because it carries the highest variance among the four ETFs, and the optimizer substitutes it with the lower-variance alternatives 0056 and 00646.


Q2 — GMVP: Monthly Returns

Step 1 · Compute Monthly Returns

# Use last price of each month
insample$YearMonth <- format(insample$Date, "%Y-%m")
monthly_price <- insample %>%
  group_by(YearMonth) %>%
  slice_tail(n = 1) %>%
  ungroup() %>%
  arrange(YearMonth) %>%
  select(YearMonth, X0050, X0056, X006205, X00646)

# Monthly simple returns
monthly_ret <- data.frame(
  X0050   = diff(monthly_price$X0050)   / head(monthly_price$X0050,   -1),
  X0056   = diff(monthly_price$X0056)   / head(monthly_price$X0056,   -1),
  X006205 = diff(monthly_price$X006205) / head(monthly_price$X006205, -1),
  X00646  = diff(monthly_price$X00646)  / head(monthly_price$X00646,  -1)
)
cat("Monthly return observations:", nrow(monthly_ret), "\n")
Monthly return observations: 36 
mu_m  <- colMeans(monthly_ret)
cov_m <- cov(monthly_ret)

cat("\nMean Monthly Returns:\n")

Mean Monthly Returns:
print(round(mu_m, 6))
    X0050     X0056   X006205    X00646 
 0.008820  0.007087 -0.005355  0.004511 
cat("\nMonthly Std Deviations:\n")

Monthly Std Deviations:
print(round(apply(monthly_ret, 2, sd), 6))
   X0050    X0056  X006205   X00646 
0.034280 0.030134 0.049409 0.029335 
cat("\nCovariance Matrix (Monthly):\n")

Covariance Matrix (Monthly):
print(round(cov_m, 8))
             X0050      X0056    X006205     X00646
X0050   0.00117515 0.00086610 0.00084722 0.00039285
X0056   0.00086610 0.00090808 0.00055533 0.00035725
X006205 0.00084722 0.00055533 0.00244129 0.00067363
X00646  0.00039285 0.00035725 0.00067363 0.00086052
cat("\nCorrelation Matrix (Monthly):\n")

Correlation Matrix (Monthly):
print(round(cor(monthly_ret), 4))
         X0050  X0056 X006205 X00646
X0050   1.0000 0.8384  0.5002 0.3907
X0056   0.8384 1.0000  0.3730 0.4041
X006205 0.5002 0.3730  1.0000 0.4648
X00646  0.3907 0.4041  0.4648 1.0000

Step 2 · GMVP Weights (Monthly)

cov_inv_m <- solve(cov_m)
w_gmvp_m  <- (cov_inv_m %*% ones) / as.numeric(t(ones) %*% cov_inv_m %*% ones)
names(w_gmvp_m) <- c("0050", "0056", "006205", "00646")

cat("=== GMVP Weights (Monthly Returns) ===\n")
=== GMVP Weights (Monthly Returns) ===
print(round(w_gmvp_m, 6))
            [,1]
X0050   0.003184
X0056   0.474049
X006205 0.001204
X00646  0.521563
attr(,"names")
[1] "0050"   "0056"   "006205" "00646" 
cat("\nSum of weights:", round(sum(w_gmvp_m), 6), "\n")

Sum of weights: 1 

Step 3 · Portfolio Return & Risk

ret_gmvp_m <- as.numeric(t(w_gmvp_m) %*% mu_m)
std_gmvp_m <- sqrt(as.numeric(t(w_gmvp_m) %*% cov_m %*% w_gmvp_m))

results_q2 <- data.frame(
  Metric = c("Optimal Weight — 0050", "Optimal Weight — 0056",
             "Optimal Weight — 006205", "Optimal Weight — 00646",
             "Portfolio Monthly Return", "Portfolio Monthly Std Dev"),
  Value  = c(round(w_gmvp_m, 6), round(ret_gmvp_m, 6), round(std_gmvp_m, 6))
)

kable(results_q2, caption = "Q2: GMVP Results (Monthly Returns)", align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Q2: GMVP Results (Monthly Returns)
Metric Value
Optimal Weight — 0050 0.003184
Optimal Weight — 0056 0.474049
Optimal Weight — 006205 0.001204
Optimal Weight — 00646 0.521563
Portfolio Monthly Return 0.005734
Portfolio Monthly Std Dev 0.024904

Key observation: 006205 receives near-zero weight in both Q1 and Q2. This ETF has the highest variance and negative mean monthly return during this period, making it unattractive for the minimum-variance objective.


Q3 — Tangency Portfolio

Using monthly returns from Q2. Risk-free rate is assumed to be zero.

Theory

The Tangency Portfolio maximizes the Sharpe ratio:

\[\max_{\mathbf{w}} \; \frac{\mathbf{w}^\top \boldsymbol{\mu} - r_f}{\sqrt{\mathbf{w}^\top \Sigma \mathbf{w}}} \quad \text{subject to} \quad \mathbf{1}^\top \mathbf{w} = 1\]

With \(r_f = 0\), the closed-form solution is:

\[\mathbf{w}^{tang} = \frac{\Sigma^{-1} \boldsymbol{\mu}}{\mathbf{1}^\top \Sigma^{-1} \boldsymbol{\mu}}\]

Tangency Portfolio Weights

rf <- 0  # risk-free rate
excess_mu_m <- mu_m - rf

w_tang <- (cov_inv_m %*% excess_mu_m) / as.numeric(t(ones) %*% cov_inv_m %*% excess_mu_m)
names(w_tang) <- c("0050", "0056", "006205", "00646")

cat("=== Tangency Portfolio Weights (Monthly, Rf=0) ===\n")
=== Tangency Portfolio Weights (Monthly, Rf=0) ===
print(round(w_tang, 6))
             [,1]
X0050    1.305054
X0056   -0.157681
X006205 -0.847532
X00646   0.700159
attr(,"names")
[1] "0050"   "0056"   "006205" "00646" 
cat("\nSum of weights:", round(sum(w_tang), 6), "\n")

Sum of weights: 1 

Performance

ret_tang   <- as.numeric(t(w_tang) %*% mu_m)
std_tang   <- sqrt(as.numeric(t(w_tang) %*% cov_m %*% w_tang))
sharpe_tang <- ret_tang / std_tang

results_q3 <- data.frame(
  Metric = c("Tangency Weight — 0050", "Tangency Weight — 0056",
             "Tangency Weight — 006205", "Tangency Weight — 00646",
             "Portfolio Monthly Return", "Portfolio Monthly Std Dev",
             "Sharpe Ratio (Rf=0)"),
  Value  = c(round(w_tang, 6), round(ret_tang, 6),
             round(std_tang, 6), round(sharpe_tang, 6))
)

kable(results_q3, caption = "Q3: Tangency Portfolio Results (Monthly Returns, Rf=0)", align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Q3: Tangency Portfolio Results (Monthly Returns, Rf=0)
Metric Value
Tangency Weight — 0050 1.305054
Tangency Weight — 0056 -0.157681
Tangency Weight — 006205 -0.847532
Tangency Weight — 00646 0.700159
Portfolio Monthly Return 0.018090
Portfolio Monthly Std Dev 0.044236
Sharpe Ratio (Rf=0) 0.408940

Summary Comparison

summary_df <- data.frame(
  Portfolio = c("GMVP (Daily)", "GMVP (Monthly)", "Tangency (Monthly)"),
  W_0050    = c(round(w_gmvp_d[1], 4), round(w_gmvp_m[1], 4), round(w_tang[1], 4)),
  W_0056    = c(round(w_gmvp_d[2], 4), round(w_gmvp_m[2], 4), round(w_tang[2], 4)),
  W_006205  = c(round(w_gmvp_d[3], 4), round(w_gmvp_m[3], 4), round(w_tang[3], 4)),
  W_00646   = c(round(w_gmvp_d[4], 4), round(w_gmvp_m[4], 4), round(w_tang[4], 4)),
  Return    = c(round(ret_gmvp_d, 6),  round(ret_gmvp_m, 6),  round(ret_tang, 6)),
  Std_Dev   = c(round(std_gmvp_d, 6),  round(std_gmvp_m, 6),  round(std_tang, 6))
)

kable(summary_df,
      col.names = c("Portfolio", "0050", "0056", "006205", "00646", "Return", "Std Dev"),
      caption   = "Summary: All Three Portfolios",
      align     = "lrrrrrr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered"), full_width = TRUE) %>%
  add_header_above(c(" " = 1, "Weights" = 4, "Performance" = 2))
Summary: All Three Portfolios
Weights
Performance
Portfolio 0050 0056 006205 00646 Return Std Dev
GMVP (Daily) -0.2194 0.7284 0.1076 0.3834 0.000254 0.005905
GMVP (Monthly) 0.0032 0.4740 0.0012 0.5216 0.005734 0.024904
Tangency (Monthly) 1.3051 -0.1577 -0.8475 0.7002 0.018090 0.044236

Efficient Frontier Visualization (Monthly)

set.seed(42)
N <- 5000

# Random portfolios
rand_weights <- matrix(rnorm(N * 4), N, 4)
rand_weights <- rand_weights / rowSums(abs(rand_weights))  # allow short, normalize
# For a cleaner plot: long-only random portfolios
rand_w_lo <- t(apply(matrix(runif(N * 4), N, 4), 1, function(x) x / sum(x)))

rand_ret <- rand_w_lo %*% mu_m
rand_std <- apply(rand_w_lo, 1, function(w) sqrt(t(w) %*% cov_m %*% w))

plot(rand_std, rand_ret,
     pch = 20, col = rgb(0.4, 0.6, 0.9, 0.3), cex = 0.5,
     xlab = "Monthly Std Dev", ylab = "Monthly Return",
     main = "Efficient Frontier — Taiwan ETFs (Monthly, 2016–2018)",
     xlim = c(0.01, 0.07), ylim = c(-0.02, 0.025))

# Plot GMVP
points(std_gmvp_m, ret_gmvp_m,
       pch = 17, col = "forestgreen", cex = 2)

# Plot Tangency
points(std_tang, ret_tang,
       pch = 8, col = "red", cex = 2)

# Individual ETFs
ind_stds <- apply(monthly_ret, 2, sd)
ind_rets <- colMeans(monthly_ret)
points(ind_stds, ind_rets, pch = 15, col = "orange", cex = 1.5)
text(ind_stds, ind_rets,
     labels = c("0050","0056","006205","00646"),
     pos = 4, cex = 0.75, col = "darkorange4")

# Capital Market Line (from tangency)
abline(a = 0, b = sharpe_tang, col = "red", lty = 2)

legend("topleft",
       legend = c("Random Portfolios (long-only)", "GMVP", "Tangency Portfolio",
                  "Individual ETFs", "Capital Market Line"),
       pch    = c(20, 17, 8, 15, NA),
       lty    = c(NA, NA, NA, NA, 2),
       col    = c(rgb(0.4, 0.6, 0.9, 0.6), "forestgreen", "red", "orange", "red"),
       cex    = 0.8, bg = "white")


Key Takeaways

  1. Q1 (Daily GMVP): The optimal daily weights are 0050 = –0.2194, 0056 = 0.7284, 006205 = 0.1076, 00646 = 0.3834. 0050 has a negative weight because its high variance can be partially hedged. The GMVP achieves a daily return of 0.000254 and daily std dev of 0.005905.

  2. Q2 (Monthly GMVP): With monthly data, weights shift to near-zero for 006205 (negative mean return) and converge to a near 50/50 split between 0056 and 00646. The GMVP monthly return is 0.5734% with a monthly std dev of 2.490%.

  3. Q3 (Tangency Portfolio): With Rf = 0, the tangency portfolio loads heavily on 0050 (130.5%) and 00646 (70.0%), shorting 0056 and 006205. It achieves a Sharpe ratio of 0.4089, higher than any individual ETF, and a monthly return of 1.809% with std dev 4.424%.


Analysis period: 2015/12/14 – 2018/12/28  ·  Risk-free rate: 0  ·  Short-selling allowed (analytical solution)