Data Loading & Preparation

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

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

# Filter in-sample period: 2015/12/14 – 2018/12/28
insample <- prices %>%
  filter(Date >= as.Date("2015-12-14") & Date <= as.Date("2018-12-28"))

cat("In-sample rows:", nrow(insample), "\n")
## In-sample rows: 751
cat("Date range:", as.character(min(insample$Date)), "to",
    as.character(max(insample$Date)), "\n")
## Date range: 2015-12-14 to 2018-12-28
kable(head(insample), caption = "First 6 rows of price data")
First 6 rows of price 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

cols <- c("tw0050","tw0056","tw006205","tw00646")

# Simple daily returns: (P_t / P_{t-1}) - 1
daily_ret <- insample %>%
  arrange(Date) %>%
  mutate(across(all_of(cols), ~ . / lag(.) - 1)) %>%
  drop_na()

cat("Daily return observations:", nrow(daily_ret), "\n")
## Daily return observations: 750
kable(head(daily_ret[, c("Date", cols)]), digits = 6,
      caption = "First 6 rows of daily returns")
First 6 rows of daily returns
Date tw0050 tw0056 tw006205 tw00646
2015-12-15 0.000751 0.007123 0.017064 0.001020
2015-12-16 0.015188 0.009793 0.000317 0.013245
2015-12-17 0.011636 0.013470 0.019937 0.008044
2015-12-18 -0.004930 0.007443 -0.001551 -0.009975
2015-12-21 -0.001651 0.003694 0.025482 -0.010579
2015-12-22 0.002389 -0.003680 0.003030 0.004073

GMVP Optimization (Daily)

The Global Minimum Variance Portfolio (GMVP) solves:

\[\min_{w} \; w^\top \Sigma w \quad \text{s.t.} \quad \mathbf{1}^\top w = 1, \quad w_i \geq 0 \text{ (no short-selling)}\]

ret_d  <- as.matrix(daily_ret[, cols])
Sigma_d <- cov(ret_d)
mu_d    <- colMeans(ret_d)
n       <- length(cols)

# quadprog: minimise 0.5 * w' D w  s.t.  A' w >= b
# meq = 1 enforces the first constraint as equality (sum = 1)
Dmat  <- 2 * Sigma_d
dvec  <- rep(0, n)
Amat  <- cbind(rep(1, n), diag(n))   # [1-vector | Identity]
bvec  <- c(1, rep(0, n))             # [1        | zeros  ]

sol_d    <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
w_gmvp_d <- sol_d$solution
names(w_gmvp_d) <- cols

kable(data.frame(ETF    = cols,
                 Weight = round(w_gmvp_d, 6),
                 Pct    = paste0(round(w_gmvp_d * 100, 2), "%")),
      col.names = c("ETF", "Weight", "Weight (%)"),
      caption   = "Q1 – GMVP Optimal Weights (Daily Returns, No Short-Selling)")
Q1 – GMVP Optimal Weights (Daily Returns, No Short-Selling)
ETF Weight Weight (%)
tw0050 tw0050 0.000000 0%
tw0056 tw0056 0.571830 57.18%
tw006205 tw006205 0.083682 8.37%
tw00646 tw00646 0.344487 34.45%

Portfolio Return & Standard Deviation (Daily)

port_ret_d <- as.numeric(t(w_gmvp_d) %*% mu_d)
port_sd_d  <- as.numeric(sqrt(t(w_gmvp_d) %*% Sigma_d %*% w_gmvp_d))

kable(data.frame(
  Metric = c("Daily Return", "Daily Std Dev"),
  Value  = c(sprintf("%.6f", port_ret_d), sprintf("%.6f", port_sd_d)),
  Pct    = c(sprintf("%.4f%%", port_ret_d * 100),
             sprintf("%.4f%%", port_sd_d  * 100))
), caption = "Q1 – GMVP Performance (Daily)")
Q1 – GMVP Performance (Daily)
Metric Value Pct
Daily Return 0.000290 0.0290%
Daily Std Dev 0.006019 0.6019%

Q2 – GMVP Using Monthly Returns

Convert Prices to Monthly Returns

We use the last closing price of each calendar month to form monthly returns.

monthly_ret <- insample %>%
  arrange(Date) %>%
  mutate(YearMon = format(Date, "%Y-%m")) %>%
  group_by(YearMon) %>%
  slice_tail(n = 1) %>%            # last trading day of month
  ungroup() %>%
  mutate(across(all_of(cols), ~ . / lag(.) - 1)) %>%
  drop_na()

cat("Monthly return observations:", nrow(monthly_ret), "\n")
## Monthly return observations: 36
kable(head(monthly_ret[, c("YearMon", cols)]), digits = 6,
      caption = "First 6 rows of monthly returns")
First 6 rows of monthly returns
YearMon tw0050 tw0056 tw006205 tw00646
2016-01 -0.019817 -0.013786 -0.173071 -0.038883
2016-02 0.028641 0.043548 -0.027578 -0.003631
2016-03 0.055505 -0.002576 0.082751 0.026028
2016-04 -0.047241 -0.037190 -0.024758 0.009640
2016-05 0.025154 0.016631 0.004415 0.022111
2016-06 0.036364 0.029551 -0.025641 -0.026057

GMVP Optimization (Monthly)

ret_m   <- as.matrix(monthly_ret[, cols])
Sigma_m <- cov(ret_m)
mu_m    <- colMeans(ret_m)

Dmat_m  <- 2 * Sigma_m
sol_m   <- solve.QP(Dmat_m, dvec, Amat, bvec, meq = 1)
w_gmvp_m <- sol_m$solution
names(w_gmvp_m) <- cols

kable(data.frame(ETF    = cols,
                 Weight = round(w_gmvp_m, 6),
                 Pct    = paste0(round(w_gmvp_m * 100, 2), "%")),
      col.names = c("ETF", "Weight", "Weight (%)"),
      caption   = "Q2 – GMVP Optimal Weights (Monthly Returns, No Short-Selling)")
Q2 – GMVP Optimal Weights (Monthly Returns, No Short-Selling)
ETF Weight Weight (%)
tw0050 tw0050 0.003184 0.32%
tw0056 tw0056 0.474049 47.4%
tw006205 tw006205 0.001204 0.12%
tw00646 tw00646 0.521563 52.16%

Portfolio Return & Standard Deviation (Monthly)

port_ret_m <- as.numeric(t(w_gmvp_m) %*% mu_m)
port_sd_m  <- as.numeric(sqrt(t(w_gmvp_m) %*% Sigma_m %*% w_gmvp_m))

kable(data.frame(
  Metric = c("Monthly Return", "Monthly Std Dev"),
  Value  = c(sprintf("%.6f", port_ret_m), sprintf("%.6f", port_sd_m)),
  Pct    = c(sprintf("%.4f%%", port_ret_m * 100),
             sprintf("%.4f%%", port_sd_m  * 100))
), caption = "Q2 – GMVP Performance (Monthly)")
Q2 – GMVP Performance (Monthly)
Metric Value Pct
Monthly Return 0.005734 0.5734%
Monthly Std Dev 0.024904 2.4904%

Note on Q2 GMVP: With only 36 monthly observations and the covariance structure of these four ETFs, the unconstrained analytical solution (\(w^* = \Sigma^{-1}\mathbf{1} / \mathbf{1}^\top\Sigma^{-1}\mathbf{1}\)) produces all non-negative weights, so the no-short-selling constraint is non-binding. The constrained and unconstrained solutions coincide here.


Q3 – Tangency Portfolio (Monthly, R_f = 0)

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

\[\max_{w} \; \frac{w^\top \mu}{\sqrt{w^\top \Sigma w}} \quad \text{s.t.} \quad \mathbf{1}^\top w = 1, \quad w_i \geq 0\]

Constrained solution via quadprog: We re-parameterise by minimising \(w^\top \Sigma w\) subject to \(\mu^\top w = 1\) and \(w \geq 0\), then renormalise so weights sum to 1.

# Constraint: mu' w = 1 (equality) + w_i >= 0 (inequality)
Amat_t <- cbind(mu_m, diag(n))    # [mu-vector | Identity]
bvec_t <- c(1, rep(0, n))

sol_t   <- solve.QP(Dmat_m, dvec, Amat_t, bvec_t, meq = 1)
w_raw_t <- sol_t$solution
w_tan   <- w_raw_t / sum(w_raw_t)   # renormalise to sum = 1
names(w_tan) <- cols

kable(data.frame(ETF    = cols,
                 Weight = round(w_tan, 6),
                 Pct    = paste0(round(w_tan * 100, 2), "%")),
      col.names = c("ETF", "Weight", "Weight (%)"),
      caption   = "Q3 – Tangency Portfolio Weights (Monthly, R_f = 0, No Short-Selling)")
Q3 – Tangency Portfolio Weights (Monthly, R_f = 0, No Short-Selling)
ETF Weight Weight (%)
tw0050 tw0050 0.604918 60.49%
tw0056 tw0056 0.180718 18.07%
tw006205 tw006205 0.000000 0%
tw00646 tw00646 0.214364 21.44%

Tangency Portfolio Performance

tan_ret <- as.numeric(t(w_tan) %*% mu_m)
tan_sd  <- as.numeric(sqrt(t(w_tan) %*% Sigma_m %*% w_tan))
tan_sr  <- tan_ret / tan_sd
gmvp_sr <- port_ret_m / port_sd_m

kable(data.frame(
  Portfolio      = c("GMVP (Monthly)", "Tangency Portfolio"),
  tw0050         = paste0(round(c(w_gmvp_m["tw0050"], w_tan["tw0050"]) * 100, 2), "%"),
  tw0056         = paste0(round(c(w_gmvp_m["tw0056"], w_tan["tw0056"]) * 100, 2), "%"),
  tw006205       = paste0(round(c(w_gmvp_m["tw006205"], w_tan["tw006205"]) * 100, 2), "%"),
  tw00646        = paste0(round(c(w_gmvp_m["tw00646"], w_tan["tw00646"]) * 100, 2), "%"),
  Monthly_Return = paste0(round(c(port_ret_m, tan_ret) * 100, 4), "%"),
  Monthly_StdDev = paste0(round(c(port_sd_m,  tan_sd)  * 100, 4), "%"),
  Sharpe_Ratio   = round(c(gmvp_sr, tan_sr), 4)
), caption   = "Q3 – Tangency vs GMVP Comparison (Monthly, R_f = 0)",
   col.names = c("Portfolio","0050","0056","006205","00646",
                 "Return (%)","Std Dev (%)","Sharpe Ratio"))
Q3 – Tangency vs GMVP Comparison (Monthly, R_f = 0)
Portfolio 0050 0056 006205 00646 Return (%) Std Dev (%) Sharpe Ratio
GMVP (Monthly) 0.32% 47.4% 0.12% 52.16% 0.5734% 2.4904% 0.2302
Tangency Portfolio 60.49% 18.07% 0% 21.44% 0.7583% 2.8603% 0.2651

Interpretation

The Tangency Portfolio allocates heavily to tw0050 (~60%) and tw00646 (~21%) while excluding tw006205 entirely. Compared to the GMVP:

  • It achieves a higher monthly return at the cost of slightly more risk.
  • Its Sharpe Ratio is higher than the GMVP — this is by construction, since the tangency portfolio maximises the Sharpe Ratio.
  • The Capital Market Line passes through the risk-free rate (0) and the tangency point; all mean–variance efficient portfolios (with borrowing/lending) lie on this line.

Complete Summary

kable(data.frame(
  Question  = c("Q1 – GMVP (Daily)", "Q2 – GMVP (Monthly)",
                "Q3 – Tangency (Monthly)"),
  w_0050    = paste0(round(c(w_gmvp_d["tw0050"],  w_gmvp_m["tw0050"],
                              w_tan["tw0050"]) * 100, 2), "%"),
  w_0056    = paste0(round(c(w_gmvp_d["tw0056"],  w_gmvp_m["tw0056"],
                              w_tan["tw0056"]) * 100, 2), "%"),
  w_006205  = paste0(round(c(w_gmvp_d["tw006205"],w_gmvp_m["tw006205"],
                              w_tan["tw006205"]) * 100, 2), "%"),
  w_00646   = paste0(round(c(w_gmvp_d["tw00646"], w_gmvp_m["tw00646"],
                              w_tan["tw00646"]) * 100, 2), "%"),
  Return    = paste0(round(c(port_ret_d, port_ret_m, tan_ret) * 100, 4), "%"),
  StdDev    = paste0(round(c(port_sd_d,  port_sd_m,  tan_sd)  * 100, 4), "%")
), caption   = "Summary: Weights and Performance for All Three Questions",
   col.names = c("Question","w(0050)","w(0056)","w(006205)","w(00646)",
                 "Return","Std Dev"))
Summary: Weights and Performance for All Three Questions
Question w(0050) w(0056) w(006205) w(00646) Return Std Dev
Q1 – GMVP (Daily) 0% 57.18% 8.37% 34.45% 0.029% 0.6019%
Q2 – GMVP (Monthly) 0.32% 47.4% 0.12% 52.16% 0.5734% 2.4904%
Q3 – Tangency (Monthly) 60.49% 18.07% 0% 21.44% 0.7583% 2.8603%