Data Loading and Preparation

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

# Load data
etf <- read.csv("myetf4.csv", stringsAsFactors = FALSE)
colnames(etf) <- c("Date", "tw0050", "tw0056", "tw006205", "tw00646")
etf$Date <- as.Date(etf$Date, format = "%Y/%m/%d")

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

cat("In-sample period:", as.character(min(etf_is$Date)), "to", as.character(max(etf_is$Date)), "\n")
## In-sample period: 2015-12-14 to 2018-12-28
cat("Number of observations:", nrow(etf_is), "\n")
## Number of observations: 751
head(etf_is) %>%
  kable(caption = "First 6 rows of in-sample data") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
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

# Compute daily simple returns
prices <- etf_is[, c("tw0050","tw0056","tw006205","tw00646")]
daily_ret <- as.data.frame(apply(prices, 2, function(x) diff(x) / head(x, -1)))

cat("Number of daily return observations:", nrow(daily_ret), "\n")
## Number of daily return observations: 750
# Summary statistics
summary_daily <- data.frame(
  ETF  = colnames(daily_ret),
  Mean = round(colMeans(daily_ret) * 100, 4),
  StdDev = round(apply(daily_ret, 2, sd) * 100, 4)
)
kable(summary_daily, caption = "Daily Return Summary (%)", col.names = c("ETF","Mean (%)","Std Dev (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Daily Return Summary (%)
ETF Mean (%) Std Dev (%)
tw0050 tw0050 0.0463 0.8853
tw0056 tw0056 0.0385 0.6728
tw006205 tw006205 -0.0212 1.1420
tw00646 tw00646 0.0255 0.7683

GMVP Computation (Daily)

The Global Minimum Variance Portfolio (GMVP) minimizes portfolio variance subject to weights summing to 1.

The analytic solution is:

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

# Covariance matrix
Sigma_d <- cov(daily_ret)

cat("Covariance matrix (daily):\n")
## Covariance matrix (daily):
print(round(Sigma_d * 10000, 6))
##            tw0050   tw0056 tw006205  tw00646
## tw0050   0.783706 0.455916 0.446726 0.366339
## tw0056   0.455916 0.452641 0.267367 0.235354
## tw006205 0.446726 0.267367 1.304184 0.291037
## tw00646  0.366339 0.235354 0.291037 0.590289
# GMVP weights
ones <- rep(1, 4)
Sigma_inv_d <- solve(Sigma_d)
w_gmvp_d <- (Sigma_inv_d %*% ones) / as.numeric(t(ones) %*% Sigma_inv_d %*% ones)

# Results table
gmvp_d_tbl <- data.frame(
  ETF    = colnames(daily_ret),
  Weight = round(as.numeric(w_gmvp_d) * 100, 4)
)
kable(gmvp_d_tbl, caption = "GMVP Weights (Daily Returns, %)",
      col.names = c("ETF","Weight (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
GMVP Weights (Daily Returns, %)
ETF Weight (%)
tw0050 -21.9358
tw0056 72.8372
tw006205 10.7623
tw00646 38.3363
cat("\nSum of weights:", round(sum(w_gmvp_d), 6), "\n")
## 
## Sum of weights: 1
# Portfolio return and std dev
gmvp_d_ret  <- as.numeric(t(w_gmvp_d) %*% colMeans(daily_ret))
gmvp_d_sd   <- sqrt(as.numeric(t(w_gmvp_d) %*% Sigma_d %*% w_gmvp_d))

cat("\nGMVP Daily Expected Return:", round(gmvp_d_ret * 100, 6), "%\n")
## 
## GMVP Daily Expected Return: 0.025366 %
cat("GMVP Daily Std Deviation:  ", round(gmvp_d_sd  * 100, 6), "%\n")
## GMVP Daily Std Deviation:   0.590494 %
# Annualized (assuming 252 trading days)
cat("\nAnnualized Expected Return:", round(gmvp_d_ret * 252 * 100, 4), "%\n")
## 
## Annualized Expected Return: 6.3923 %
cat("Annualized Std Deviation:  ", round(gmvp_d_sd * sqrt(252) * 100, 4), "%\n")
## Annualized Std Deviation:   9.3738 %

Summary – Q1 Results

q1_res <- data.frame(
  Metric = c("GMVP Daily Expected Return (%)", "GMVP Daily Std Deviation (%)",
             "Annualized Return (%)", "Annualized Std Dev (%)"),
  Value  = c(round(gmvp_d_ret*100, 6), round(gmvp_d_sd*100, 6),
             round(gmvp_d_ret*252*100, 4), round(gmvp_d_sd*sqrt(252)*100, 4))
)
kable(q1_res, caption = "Q1: GMVP Summary (Daily Returns)") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Q1: GMVP Summary (Daily Returns)
Metric Value
GMVP Daily Expected Return (%) 0.025366
GMVP Daily Std Deviation (%) 0.590494
Annualized Return (%) 6.392300
Annualized Std Dev (%) 9.373800

Q2: GMVP Using Monthly Returns

Compute Monthly Returns

Monthly returns are computed from the last trading day price of each month.

# Add year-month column, take last price of each month
etf_is_m <- etf_is %>%
  mutate(YM = format(Date, "%Y-%m")) %>%
  group_by(YM) %>%
  slice_tail(n = 1) %>%
  ungroup() %>%
  arrange(Date)

# Monthly returns from end-of-month prices
prices_m <- as.matrix(etf_is_m[, c("tw0050","tw0056","tw006205","tw00646")])
monthly_ret <- as.data.frame(apply(prices_m, 2, function(x) diff(x) / head(x,-1)))

cat("Number of monthly return observations:", nrow(monthly_ret), "\n")
## Number of monthly return observations: 36
summary_monthly <- data.frame(
  ETF    = colnames(monthly_ret),
  Mean   = round(colMeans(monthly_ret) * 100, 4),
  StdDev = round(apply(monthly_ret, 2, sd) * 100, 4)
)
kable(summary_monthly, caption = "Monthly Return Summary (%)",
      col.names = c("ETF","Mean (%)","Std Dev (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Monthly Return Summary (%)
ETF Mean (%) Std Dev (%)
tw0050 tw0050 0.8820 3.4280
tw0056 tw0056 0.7087 3.0134
tw006205 tw006205 -0.5355 4.9409
tw00646 tw00646 0.4511 2.9335

GMVP Computation (Monthly)

Sigma_m <- cov(monthly_ret)

cat("Covariance matrix (monthly):\n")
## Covariance matrix (monthly):
print(round(Sigma_m * 10000, 6))
##             tw0050   tw0056  tw006205  tw00646
## tw0050   11.751458 8.661004  8.472189 3.928466
## tw0056    8.661004 9.080806  5.553289 3.572509
## tw006205  8.472189 5.553289 24.412877 6.736296
## tw00646   3.928466 3.572509  6.736296 8.605161
Sigma_inv_m <- solve(Sigma_m)
w_gmvp_m <- (Sigma_inv_m %*% ones) / as.numeric(t(ones) %*% Sigma_inv_m %*% ones)

gmvp_m_tbl <- data.frame(
  ETF    = colnames(monthly_ret),
  Weight = round(as.numeric(w_gmvp_m) * 100, 4)
)
kable(gmvp_m_tbl, caption = "GMVP Weights (Monthly Returns, %)",
      col.names = c("ETF","Weight (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
GMVP Weights (Monthly Returns, %)
ETF Weight (%)
tw0050 0.3184
tw0056 47.4049
tw006205 0.1204
tw00646 52.1563
cat("\nSum of weights:", round(sum(w_gmvp_m), 6), "\n")
## 
## Sum of weights: 1
gmvp_m_ret  <- as.numeric(t(w_gmvp_m) %*% colMeans(monthly_ret))
gmvp_m_sd   <- sqrt(as.numeric(t(w_gmvp_m) %*% Sigma_m %*% w_gmvp_m))

cat("\nGMVP Monthly Expected Return:", round(gmvp_m_ret * 100, 6), "%\n")
## 
## GMVP Monthly Expected Return: 0.573367 %
cat("GMVP Monthly Std Deviation:  ", round(gmvp_m_sd  * 100, 6), "%\n")
## GMVP Monthly Std Deviation:   2.490441 %
# Annualized (12 months)
cat("\nAnnualized Expected Return:", round(gmvp_m_ret * 12 * 100, 4), "%\n")
## 
## Annualized Expected Return: 6.8804 %
cat("Annualized Std Deviation:  ", round(gmvp_m_sd * sqrt(12) * 100, 4), "%\n")
## Annualized Std Deviation:   8.6271 %

Summary – Q2 Results

q2_res <- data.frame(
  Metric = c("GMVP Monthly Expected Return (%)", "GMVP Monthly Std Deviation (%)",
             "Annualized Return (%)", "Annualized Std Dev (%)"),
  Value  = c(round(gmvp_m_ret*100, 6), round(gmvp_m_sd*100, 6),
             round(gmvp_m_ret*12*100, 4), round(gmvp_m_sd*sqrt(12)*100, 4))
)
kable(q2_res, caption = "Q2: GMVP Summary (Monthly Returns)") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Q2: GMVP Summary (Monthly Returns)
Metric Value
GMVP Monthly Expected Return (%) 0.573367
GMVP Monthly Std Deviation (%) 2.490441
Annualized Return (%) 6.880400
Annualized Std Dev (%) 8.627100

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

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

\[w_{tan} = \frac{\Sigma^{-1}\mu}{\mathbf{1}^T \Sigma^{-1}\mu}\]

mu_m <- colMeans(monthly_ret)   # mean monthly returns
Rf   <- 0                        # risk-free rate

# Excess returns (= mu since Rf = 0)
excess_m <- mu_m - Rf

# Tangency portfolio weights
w_tan_raw <- Sigma_inv_m %*% excess_m
w_tan     <- w_tan_raw / sum(w_tan_raw)

tan_tbl <- data.frame(
  ETF    = colnames(monthly_ret),
  Weight = round(as.numeric(w_tan) * 100, 4)
)
kable(tan_tbl, caption = "Tangency Portfolio Weights (Monthly Returns, Rf=0, %)",
      col.names = c("ETF","Weight (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Tangency Portfolio Weights (Monthly Returns, Rf=0, %)
ETF Weight (%)
tw0050 130.5054
tw0056 -15.7681
tw006205 -84.7532
tw00646 70.0159
cat("\nSum of weights:", round(sum(w_tan), 6), "\n")
## 
## Sum of weights: 1
tan_ret <- as.numeric(t(w_tan) %*% mu_m)
tan_sd  <- sqrt(as.numeric(t(w_tan) %*% Sigma_m %*% w_tan))
tan_sr  <- (tan_ret - Rf) / tan_sd

cat("\nTangency Portfolio Monthly Return: ", round(tan_ret * 100, 6), "%\n")
## 
## Tangency Portfolio Monthly Return:  1.809002 %
cat("Tangency Portfolio Monthly Std Dev:", round(tan_sd  * 100, 6), "%\n")
## Tangency Portfolio Monthly Std Dev: 4.423638 %
cat("Sharpe Ratio (monthly):            ", round(tan_sr, 6), "\n")
## Sharpe Ratio (monthly):             0.40894
# Annualized
cat("\nAnnualized Return:",   round(tan_ret * 12 * 100, 4),       "%\n")
## 
## Annualized Return: 21.708 %
cat("Annualized Std Dev:",   round(tan_sd * sqrt(12) * 100, 4),   "%\n")
## Annualized Std Dev: 15.3239 %
cat("Annualized Sharpe:  ",  round(tan_sr * sqrt(12), 4),          "\n")
## Annualized Sharpe:   1.4166

Summary – Q3 Results

q3_res <- data.frame(
  Metric = c("Monthly Expected Return (%)", "Monthly Std Deviation (%)",
             "Monthly Sharpe Ratio",
             "Annualized Return (%)", "Annualized Std Dev (%)","Annualized Sharpe"),
  Value  = c(round(tan_ret*100, 6), round(tan_sd*100, 6), round(tan_sr, 6),
             round(tan_ret*12*100, 4), round(tan_sd*sqrt(12)*100, 4), round(tan_sr*sqrt(12),4))
)
kable(q3_res, caption = "Q3: Tangency Portfolio Summary") %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Q3: Tangency Portfolio Summary
Metric Value
Monthly Expected Return (%) 1.809002
Monthly Std Deviation (%) 4.423638
Monthly Sharpe Ratio 0.408940
Annualized Return (%) 21.708000
Annualized Std Dev (%) 15.323900
Annualized Sharpe 1.416600

Comparison: GMVP vs Tangency Portfolio

comp <- data.frame(
  Portfolio   = c("GMVP (Daily)", "GMVP (Monthly)", "Tangency (Monthly)"),
  Weights_0050  = c(round(w_gmvp_d[1]*100,2), round(w_gmvp_m[1]*100,2), round(w_tan[1]*100,2)),
  Weights_0056  = c(round(w_gmvp_d[2]*100,2), round(w_gmvp_m[2]*100,2), round(w_tan[2]*100,2)),
  Weights_006205= c(round(w_gmvp_d[3]*100,2), round(w_gmvp_m[3]*100,2), round(w_tan[3]*100,2)),
  Weights_00646 = c(round(w_gmvp_d[4]*100,2), round(w_gmvp_m[4]*100,2), round(w_tan[4]*100,2))
)
kable(comp, caption = "Portfolio Weights Comparison (%)",
      col.names = c("Portfolio","0050 (%)","0056 (%)","006205 (%)","00646 (%)")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)
Portfolio Weights Comparison (%)
Portfolio 0050 (%) 0056 (%) 006205 (%) 00646 (%)
GMVP (Daily) -21.94 72.84 10.76 38.34
GMVP (Monthly) 0.32 47.40 0.12 52.16
Tangency (Monthly) 130.51 -15.77 -84.75 70.02

Efficient Frontier (Monthly)

library(ggplot2)

# Grid over target returns
target_rets <- seq(min(mu_m), max(mu_m)*1.5, length.out = 200)

port_sd <- sapply(target_rets, function(mu_p) {
  # Lagrangian QP for minimum variance at target return
  A <- rbind(cbind(2*Sigma_m, -mu_m, -ones),
             cbind(t(mu_m),  0,     0),
             cbind(t(ones),  0,     0))
  b <- c(rep(0,4), mu_p, 1)
  tryCatch({
    sol <- solve(A, b)
    w   <- sol[1:4]
    sqrt(as.numeric(t(w) %*% Sigma_m %*% w))
  }, error = function(e) NA)
})

ef_df <- data.frame(StdDev = port_sd * 100, Return = target_rets * 100)
ef_df <- ef_df[!is.na(ef_df$StdDev), ]

# Individual ETFs
ind_df <- data.frame(
  ETF    = c("0050","0056","006205","00646"),
  StdDev = apply(monthly_ret, 2, sd) * 100,
  Return = colMeans(monthly_ret) * 100
)

ggplot(ef_df, aes(x = StdDev, y = Return)) +
  geom_path(color = "steelblue", linewidth = 1.2) +
  geom_point(data = ind_df, aes(color = ETF), size = 3, shape = 17) +
  geom_point(aes(x = gmvp_m_sd*100, y = gmvp_m_ret*100),
             color = "darkgreen", size = 4, shape = 8) +
  geom_point(aes(x = tan_sd*100, y = tan_ret*100),
             color = "red", size = 4, shape = 8) +
  annotate("text", x = gmvp_m_sd*100, y = gmvp_m_ret*100,
           label = "GMVP", hjust = -0.15, size = 3.5, color = "darkgreen") +
  annotate("text", x = tan_sd*100, y = tan_ret*100,
           label = "Tangency", hjust = -0.15, size = 3.5, color = "red") +
  labs(title = "Efficient Frontier (Monthly Returns)",
       x = "Monthly Std Deviation (%)", y = "Monthly Expected Return (%)",
       color = "ETF") +
  theme_minimal(base_size = 13)


Conclusion

GMVP (Daily) GMVP (Monthly) Tangency (Monthly, Rf=0)
0050 weight -21.94% 0.32% 130.51%
0056 weight 72.84% 47.4% -15.77%
006205 weight 10.76% 0.12% -84.75%
00646 weight 38.34% 52.16% 70.02%
E(R) monthly 0.0254% 0.5734% 1.809%
Std Dev monthly 0.5905% 2.4904% 4.4236%
Sharpe (monthly) 0.4089
  • The GMVP minimizes variance — it allocates heavily to ETFs with low individual volatility and low correlations.
  • The Tangency Portfolio (with \(R_f = 0\)) maximizes the Sharpe ratio and tilts toward ETFs with higher mean returns relative to their risk contribution.
  • Daily and monthly GMVP weights differ because the covariance structure estimated from different frequencies is not identical.