This homework solves three portfolio optimization problems using four
Taiwan ETFs: 0050, 0056,
006205, and 00646.
The in-sample period is 2015/12/14 – 2018/12/28.
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
## Date range: 2015-12-14 to 2018-12-28
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:
## X0050 X0056 X006205 X00646
## 0.000463 0.000385 -0.000212 0.000255
##
## Covariance Matrix (Daily):
## 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
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) ===
## [,1]
## X0050 -0.219358
## X0056 0.728372
## X006205 0.107623
## X00646 0.383363
## attr(,"names")
## [1] "0050" "0056" "006205" "00646"
##
## Sum of weights: 1
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)| 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.
# 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
##
## Mean Monthly Returns:
## X0050 X0056 X006205 X00646
## 0.008820 0.007087 -0.005355 0.004511
##
## Monthly Std Deviations:
## X0050 X0056 X006205 X00646
## 0.034280 0.030134 0.049409 0.029335
##
## Covariance Matrix (Monthly):
## 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
##
## Correlation Matrix (Monthly):
## 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
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) ===
## [,1]
## X0050 0.003184
## X0056 0.474049
## X006205 0.001204
## X00646 0.521563
## attr(,"names")
## [1] "0050" "0056" "006205" "00646"
##
## Sum of weights: 1
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)| 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.
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}}\]
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) ===
## [,1]
## X0050 1.305054
## X0056 -0.157681
## X006205 -0.847532
## X00646 0.700159
## attr(,"names")
## [1] "0050" "0056" "006205" "00646"
##
## Sum of weights: 1
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)| 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_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))| 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 |
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")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.
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%.
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)