Introduction
This homework analyzes four Taiwan ETFs: 0050,
0056, 006205, and
00646 using in-sample data from 2015/12/14 to
2018/12/28.
We will:
- Find the Global Minimum Variance Portfolio (GMVP)
using daily returns
- Recalculate GMVP using monthly returns
- Find the Tangency Portfolio using monthly returns
(risk-free rate = 0, short-selling allowed)
Load Libraries &
Data
library(tidyverse)
library(lubridate)
library(quadprog)
library(knitr)
library(kableExtra)
# Load data
df <- read.csv("myetf4.csv", stringsAsFactors = FALSE)
# Rename columns
colnames(df) <- c("Date", "tw0050", "tw0056", "tw006205", "tw00646")
# Parse dates (handles both "12/14/2015" and "2015-12-14" formats)
df$Date <- parse_date_time(df$Date, orders = c("mdy", "ymd"))
df$Date <- as.Date(df$Date)
# Filter in-sample period: 2015/12/14 to 2018/12/28
df_insample <- df %>%
filter(Date >= as.Date("2015-12-14") & Date <= as.Date("2018-12-28")) %>%
arrange(Date)
cat("In-sample rows:", nrow(df_insample), "\n")
## In-sample rows: 751
cat("Date range:", as.character(min(df_insample$Date)),
"to", as.character(max(df_insample$Date)), "\n")
## Date range: 2015-12-14 to 2018-12-28
# Preview
head(df_insample) %>%
kable(caption = "First 6 rows of in-sample data") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
First 6 rows of in-sample data
|
Date
|
tw0050
|
tw0056
|
tw006205
|
tw00646
|
|
2015-12-14
|
53.29
|
18.25
|
31.06
|
19.61
|
|
2015-12-15
|
53.33
|
18.38
|
31.59
|
19.63
|
|
2015-12-16
|
54.14
|
18.56
|
31.60
|
19.89
|
|
2015-12-17
|
54.77
|
18.81
|
32.23
|
20.05
|
|
2015-12-18
|
54.50
|
18.95
|
32.18
|
19.85
|
|
2015-12-21
|
54.41
|
19.02
|
33.00
|
19.64
|
Q1: GMVP Using Daily
Returns
Compute Daily
Returns
prices_daily <- df_insample[, c("tw0050", "tw0056", "tw006205", "tw00646")]
# Daily log returns
ret_daily <- as.data.frame(apply(prices_daily, 2, function(x) diff(log(x))))
cat("Number of daily return observations:", nrow(ret_daily), "\n")
## Number of daily return observations: 750
head(ret_daily) %>%
kable(caption = "First 6 rows of daily log returns", digits = 6) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
First 6 rows of daily log returns
|
tw0050
|
tw0056
|
tw006205
|
tw00646
|
|
0.000750
|
0.007098
|
0.016920
|
0.001019
|
|
0.015074
|
0.009746
|
0.000317
|
0.013158
|
|
0.011569
|
0.013380
|
0.019741
|
0.008012
|
|
-0.004942
|
0.007415
|
-0.001553
|
-0.010025
|
|
-0.001653
|
0.003687
|
0.025162
|
-0.010636
|
|
0.002386
|
-0.003687
|
0.003026
|
0.004065
|
Solve for GMVP
Weights (Daily)
The Global Minimum Variance Portfolio solves:
\[\min_w \; w^\top \Sigma w \quad
\text{subject to} \quad \mathbf{1}^\top w = 1, \; w_i \geq
0\]
n <- ncol(ret_daily)
cov_daily <- cov(ret_daily)
# quadprog: minimize 0.5 * w' Dmat w - dvec' w
Dmat <- 2 * cov_daily
dvec <- rep(0, n)
# Constraints: sum(w) = 1 (equality) + w_i >= 0 (non-negativity)
Amat <- cbind(rep(1, n), diag(n))
bvec <- c(1, rep(0, n))
sol_daily <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
weights_daily <- sol_daily$solution
names(weights_daily) <- colnames(ret_daily)
data.frame(
ETF = names(weights_daily),
Weight = round(weights_daily, 6),
Weight_pct = paste0(round(weights_daily * 100, 4), "%")
) %>%
kable(caption = "GMVP Optimal Weights (Daily Returns)",
col.names = c("ETF", "Weight", "Weight (%)")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
GMVP Optimal Weights (Daily Returns)
|
|
ETF
|
Weight
|
Weight (%)
|
|
tw0050
|
tw0050
|
0.000000
|
0%
|
|
tw0056
|
tw0056
|
0.569975
|
56.9975%
|
|
tw006205
|
tw006205
|
0.083489
|
8.3489%
|
|
tw00646
|
tw00646
|
0.346536
|
34.6536%
|
GMVP Return &
Standard Deviation (Daily)
mean_daily <- colMeans(ret_daily)
port_ret_d <- sum(weights_daily * mean_daily)
port_var_d <- as.numeric(t(weights_daily) %*% cov_daily %*% weights_daily)
port_sd_d <- sqrt(port_var_d)
data.frame(
Metric = c("Expected Daily Return", "Daily Std Deviation",
"Annualized Return (×252)", "Annualized Std Dev (×√252)"),
Value = c(
paste0(round(port_ret_d * 100, 6), "%"),
paste0(round(port_sd_d * 100, 6), "%"),
paste0(round(port_ret_d * 252 * 100, 4), "%"),
paste0(round(port_sd_d * sqrt(252) * 100, 4), "%")
)
) %>%
kable(caption = "GMVP Statistics (Daily Returns)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
GMVP Statistics (Daily Returns)
|
Metric
|
Value
|
|
Expected Daily Return
|
0.026138%
|
|
Daily Std Deviation
|
0.604032%
|
|
Annualized Return (×252)
|
6.5867%
|
|
Annualized Std Dev (×√252)
|
9.5887%
|
Q2: GMVP Using Monthly
Returns
Aggregate to Monthly
Returns
# Attach dates back (first date lost to differencing)
ret_daily_dated <- ret_daily
ret_daily_dated$Date <- df_insample$Date[-1]
# Sum daily log returns within each month → monthly log return
ret_monthly <- ret_daily_dated %>%
mutate(YearMonth = floor_date(Date, "month")) %>%
group_by(YearMonth) %>%
summarise(across(c(tw0050, tw0056, tw006205, tw00646), sum), .groups = "drop")
cat("Number of monthly return observations:", nrow(ret_monthly), "\n")
## Number of monthly return observations: 37
ret_monthly %>%
mutate(across(where(is.numeric), ~ round(.x, 6))) %>%
kable(caption = "Monthly Returns (sum of daily log returns)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
Monthly Returns (sum of daily log returns)
|
YearMonth
|
tw0050
|
tw0056
|
tw006205
|
tw00646
|
|
2015-12-01
|
0.022452
|
0.032878
|
0.030128
|
0.022688
|
|
2016-01-01
|
-0.020015
|
-0.013882
|
-0.190036
|
-0.039659
|
|
2016-02-01
|
0.028238
|
0.042627
|
-0.027966
|
-0.003637
|
|
2016-03-01
|
0.054019
|
-0.002579
|
0.079505
|
0.025695
|
|
2016-04-01
|
-0.048394
|
-0.037899
|
-0.025069
|
0.009594
|
|
2016-05-01
|
0.024843
|
0.016494
|
0.004405
|
0.021870
|
|
2016-06-01
|
0.035718
|
0.029123
|
-0.025975
|
-0.026403
|
|
2016-07-01
|
0.058709
|
0.075498
|
0.011587
|
0.030328
|
|
2016-08-01
|
0.012292
|
0.011342
|
0.027851
|
0.003910
|
|
2016-09-01
|
0.021969
|
0.018161
|
-0.037560
|
-0.023693
|
|
2016-10-01
|
0.023014
|
-0.000923
|
0.016376
|
-0.002000
|
|
2016-11-01
|
-0.012362
|
-0.022893
|
0.054592
|
0.037814
|
|
2016-12-01
|
0.001381
|
-0.008066
|
-0.057919
|
0.040147
|
|
2017-01-01
|
0.020641
|
0.020279
|
0.016894
|
-0.014456
|
|
2017-02-01
|
0.021401
|
0.038915
|
0.002909
|
0.010280
|
|
2017-03-01
|
0.007471
|
0.010275
|
-0.028731
|
0.003712
|
|
2017-04-01
|
0.011464
|
-0.011173
|
-0.016199
|
-0.003248
|
|
2017-05-01
|
0.025779
|
0.015165
|
0.032876
|
0.003711
|
|
2017-06-01
|
0.049781
|
0.029658
|
0.062331
|
0.016074
|
|
2017-07-01
|
0.020262
|
0.019996
|
0.041926
|
0.014922
|
|
2017-08-01
|
0.019474
|
0.000000
|
0.035457
|
-0.009923
|
|
2017-09-01
|
-0.021311
|
-0.005068
|
-0.004484
|
0.037371
|
|
2017-10-01
|
0.048077
|
0.021776
|
0.023478
|
0.013446
|
|
2017-11-01
|
-0.035158
|
-0.023047
|
0.011226
|
0.013268
|
|
2017-12-01
|
-0.001297
|
0.000424
|
-0.007157
|
0.015609
|
|
2018-01-01
|
0.055549
|
0.056028
|
0.082107
|
0.035366
|
|
2018-02-01
|
-0.027641
|
-0.019010
|
-0.044713
|
-0.018349
|
|
2018-03-01
|
0.007296
|
0.000000
|
-0.052814
|
-0.041594
|
|
2018-04-01
|
-0.036242
|
-0.021462
|
-0.013732
|
0.022902
|
|
2018-05-01
|
0.009313
|
0.030810
|
0.003210
|
0.028520
|
|
2018-06-01
|
0.008717
|
-0.020434
|
-0.089111
|
0.018971
|
|
2018-07-01
|
0.057414
|
0.057755
|
0.004545
|
0.033422
|
|
2018-08-01
|
0.016257
|
0.021589
|
-0.034419
|
0.039801
|
|
2018-09-01
|
-0.000593
|
-0.018088
|
0.027770
|
-0.001488
|
|
2018-10-01
|
-0.113846
|
-0.079590
|
-0.077734
|
-0.065358
|
|
2018-11-01
|
-0.010424
|
0.028194
|
-0.006855
|
0.011848
|
|
2018-12-01
|
-0.016388
|
-0.021488
|
-0.036580
|
-0.092057
|
Solve for GMVP
Weights (Monthly)
ret_m <- ret_monthly %>% select(-YearMonth)
cov_monthly <- cov(ret_m)
Dmat_m <- 2 * cov_monthly
dvec_m <- rep(0, n)
Amat_m <- cbind(rep(1, n), diag(n))
bvec_m <- c(1, rep(0, n))
sol_monthly <- solve.QP(Dmat_m, dvec_m, Amat_m, bvec_m, meq = 1)
weights_monthly <- sol_monthly$solution
names(weights_monthly) <- colnames(ret_m)
data.frame(
ETF = names(weights_monthly),
Weight = round(weights_monthly, 6),
Weight_pct = paste0(round(weights_monthly * 100, 4), "%")
) %>%
kable(caption = "GMVP Optimal Weights (Monthly Returns)",
col.names = c("ETF", "Weight", "Weight (%)")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
GMVP Optimal Weights (Monthly Returns)
|
|
ETF
|
Weight
|
Weight (%)
|
|
tw0050
|
tw0050
|
0.000000
|
0%
|
|
tw0056
|
tw0056
|
0.488816
|
48.8816%
|
|
tw006205
|
tw006205
|
0.000000
|
0%
|
|
tw00646
|
tw00646
|
0.511184
|
51.1184%
|
GMVP Return &
Standard Deviation (Monthly)
mean_monthly <- colMeans(ret_m)
port_ret_m <- sum(weights_monthly * mean_monthly)
port_var_m <- as.numeric(t(weights_monthly) %*% cov_monthly %*% weights_monthly)
port_sd_m <- sqrt(port_var_m)
data.frame(
Metric = c("Expected Monthly Return", "Monthly Std Deviation",
"Annualized Return (×12)", "Annualized Std Dev (×√12)"),
Value = c(
paste0(round(port_ret_m * 100, 6), "%"),
paste0(round(port_sd_m * 100, 6), "%"),
paste0(round(port_ret_m * 12 * 100, 4), "%"),
paste0(round(port_sd_m * sqrt(12) * 100, 4), "%")
)
) %>%
kable(caption = "GMVP Statistics (Monthly Returns)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
GMVP Statistics (Monthly Returns)
|
Metric
|
Value
|
|
Expected Monthly Return
|
0.59258%
|
|
Monthly Std Deviation
|
2.499661%
|
|
Annualized Return (×12)
|
7.111%
|
|
Annualized Std Dev (×√12)
|
8.6591%
|
Q3: Tangency Portfolio
(Monthly Returns, Rf = 0)
The Tangency Portfolio maximizes the Sharpe Ratio.
With \(R_f = 0\), the analytical
closed-form solution is:
\[w^* \propto \Sigma^{-1} \mu \quad
\text{then rescale so } \sum w_i = 1\]
Short-selling is allowed (standard textbook
formulation).
Rf <- 0
mu_m <- colMeans(ret_m)
Sigma_m <- cov(ret_m)
# Analytical solution
z <- solve(Sigma_m) %*% (mu_m - Rf)
weights_tan <- as.vector(z / sum(z))
names(weights_tan) <- colnames(ret_m)
data.frame(
ETF = names(weights_tan),
Weight = round(weights_tan, 6),
Weight_pct = paste0(round(weights_tan * 100, 4), "%")
) %>%
kable(caption = "Tangency Portfolio Weights (Monthly Returns, Rf = 0)",
col.names = c("ETF", "Weight", "Weight (%)")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
Tangency Portfolio Weights (Monthly Returns, Rf = 0)
|
|
ETF
|
Weight
|
Weight (%)
|
|
tw0050
|
tw0050
|
1.017563
|
101.7563%
|
|
tw0056
|
tw0056
|
0.099867
|
9.9867%
|
|
tw006205
|
tw006205
|
-0.764590
|
-76.459%
|
|
tw00646
|
tw00646
|
0.647159
|
64.7159%
|
Tangency Portfolio
Return, SD & Sharpe Ratio
port_ret_tan <- sum(weights_tan * mu_m)
port_var_tan <- as.numeric(t(weights_tan) %*% Sigma_m %*% weights_tan)
port_sd_tan <- sqrt(port_var_tan)
sharpe_tan <- (port_ret_tan - Rf) / port_sd_tan
data.frame(
Metric = c("Expected Monthly Return", "Monthly Std Deviation", "Monthly Sharpe Ratio",
"Annualized Return (×12)", "Annualized Std Dev (×√12)", "Annualized Sharpe (×√12)"),
Value = c(
paste0(round(port_ret_tan * 100, 6), "%"),
paste0(round(port_sd_tan * 100, 6), "%"),
round(sharpe_tan, 6),
paste0(round(port_ret_tan * 12 * 100, 4), "%"),
paste0(round(port_sd_tan * sqrt(12) * 100, 4), "%"),
round(sharpe_tan * sqrt(12), 6)
)
) %>%
kable(caption = "Tangency Portfolio Statistics (Monthly Returns, Rf = 0)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Tangency Portfolio Statistics (Monthly Returns, Rf = 0)
|
Metric
|
Value
|
|
Expected Monthly Return
|
1.673302%
|
|
Monthly Std Deviation
|
4.158934%
|
|
Monthly Sharpe Ratio
|
0.402339
|
|
Annualized Return (×12)
|
20.0796%
|
|
Annualized Std Dev (×√12)
|
14.407%
|
|
Annualized Sharpe (×√12)
|
1.393744
|
Summary Comparison
data.frame(
Portfolio = c("GMVP (Daily)", "GMVP (Monthly)", "Tangency (Monthly)"),
tw0050 = paste0(round(c(weights_daily["tw0050"],
weights_monthly["tw0050"],
weights_tan["tw0050"]) * 100, 2), "%"),
tw0056 = paste0(round(c(weights_daily["tw0056"],
weights_monthly["tw0056"],
weights_tan["tw0056"]) * 100, 2), "%"),
tw006205 = paste0(round(c(weights_daily["tw006205"],
weights_monthly["tw006205"],
weights_tan["tw006205"]) * 100, 2), "%"),
tw00646 = paste0(round(c(weights_daily["tw00646"],
weights_monthly["tw00646"],
weights_tan["tw00646"]) * 100, 2), "%"),
Ann_Return = c(
paste0(round(port_ret_d * 252 * 100, 4), "%"),
paste0(round(port_ret_m * 12 * 100, 4), "%"),
paste0(round(port_ret_tan * 12 * 100, 4), "%")
),
Ann_SD = c(
paste0(round(port_sd_d * sqrt(252) * 100, 4), "%"),
paste0(round(port_sd_m * sqrt(12) * 100, 4), "%"),
paste0(round(port_sd_tan * sqrt(12) * 100, 4), "%")
)
) %>%
kable(caption = "Portfolio Comparison Summary",
col.names = c("Portfolio", "tw0050", "tw0056", "tw006205", "tw00646",
"Ann. Return", "Ann. Std Dev")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
Portfolio Comparison Summary
|
Portfolio
|
tw0050
|
tw0056
|
tw006205
|
tw00646
|
Ann. Return
|
Ann. Std Dev
|
|
GMVP (Daily)
|
0%
|
57%
|
8.35%
|
34.65%
|
6.5867%
|
9.5887%
|
|
GMVP (Monthly)
|
0%
|
48.88%
|
0%
|
51.12%
|
7.111%
|
8.6591%
|
|
Tangency (Monthly)
|
101.76%
|
9.99%
|
-76.46%
|
64.72%
|
20.0796%
|
14.407%
|
Efficient Frontier
Visualization (Monthly)
set.seed(42)
n_sim <- 5000
sim_ret <- numeric(n_sim)
sim_sd <- numeric(n_sim)
for (i in 1:n_sim) {
w <- runif(n); w <- w / sum(w)
sim_ret[i] <- sum(w * mu_m)
sim_sd[i] <- sqrt(as.numeric(t(w) %*% Sigma_m %*% w))
}
sim_df <- data.frame(SD = sim_sd * 100, Return = sim_ret * 100)
key_pts <- data.frame(
Label = c("GMVP (Monthly)", "Tangency Portfolio"),
SD = c(port_sd_m * 100, port_sd_tan * 100),
Return = c(port_ret_m * 100, port_ret_tan * 100)
)
ggplot(sim_df, aes(x = SD, y = Return)) +
geom_point(alpha = 0.15, size = 0.7, color = "#4A90D9") +
geom_point(data = key_pts, aes(x = SD, y = Return, color = Label),
size = 5, shape = 18) +
geom_text(data = key_pts, aes(x = SD, y = Return, label = Label),
vjust = -1, size = 3.5, fontface = "bold") +
scale_color_manual(values = c("GMVP (Monthly)" = "#E74C3C",
"Tangency Portfolio" = "#27AE60")) +
labs(
title = "Efficient Frontier: 4 Taiwan ETFs (Monthly Returns)",
subtitle = "In-sample period: 2015/12/14 – 2018/12/28 | Rf = 0",
x = "Monthly Std Dev (%)",
y = "Monthly Return (%)",
color = "Portfolio"
) +
theme_minimal(base_size = 13) +
theme(legend.position = "bottom",
plot.title = element_text(face = "bold"))
