tickers <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")
getSymbols(tickers,
src = "yahoo",
from = "2010-01-01",
to = "2025-03-31",
auto.assign = TRUE)## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"
# Extract adjusted closing prices
adj_prices <- do.call(merge, lapply(tickers, function(tk) Ad(get(tk))))
colnames(adj_prices) <- tickers
head(adj_prices)## SPY QQQ EEM IWM EFA TLT IYR
## 2010-01-04 84.79635 40.29079 30.35151 51.36655 35.12845 56.13521 26.76811
## 2010-01-05 85.02084 40.29079 30.57181 51.18993 35.15940 56.49770 26.83238
## 2010-01-06 85.08069 40.04776 30.63576 51.14176 35.30801 55.74142 26.82069
## 2010-01-07 85.43984 40.07380 30.45811 51.51909 35.17179 55.83513 27.06027
## 2010-01-08 85.72417 40.40363 30.69973 51.80010 35.45043 55.81018 26.87913
## 2010-01-11 85.84389 40.23872 30.63576 51.59137 35.74147 55.50389 27.00768
## GLD
## 2010-01-04 109.80
## 2010-01-05 109.70
## 2010-01-06 111.51
## 2010-01-07 110.82
## 2010-01-08 111.37
## 2010-01-11 112.85
## SPY QQQ EEM IWM EFA TLT IYR
## 2025-03-21 557.5881 477.8112 43.52934 201.6173 80.77219 86.45229 92.23277
## 2025-03-24 567.5737 488.2964 43.72462 206.6332 80.76254 85.56583 93.50266
## 2025-03-25 568.9379 491.0829 43.59769 205.4856 81.20666 85.55630 92.46722
## 2025-03-26 562.1458 482.0467 43.33406 203.3981 80.15428 84.99394 92.97517
## 2025-03-27 560.6529 479.3000 43.54887 202.4285 80.32806 84.74613 92.68211
## 2025-03-28 549.3624 466.6811 42.73843 198.3129 79.61361 85.91851 92.54536
## GLD
## 2025-03-21 278.49
## 2025-03-24 277.25
## 2025-03-25 278.47
## 2025-03-26 278.24
## 2025-03-27 281.97
## 2025-03-28 284.06
The eight ETFs span U.S. large-cap equities (SPY), technology (QQQ), emerging markets (EEM), small-cap U.S. (IWM), developed international (EFA), long-term Treasuries (TLT), U.S. real estate (IYR), and gold (GLD), providing broad multi-asset diversification.
# Convert to monthly adjusted prices (last trading day of each month)
monthly_prices <- to.monthly(adj_prices, indexAt = "lastof", OHLC = FALSE)
# Discrete (simple) monthly returns: (P_t - P_{t-1}) / P_{t-1}
monthly_returns <- na.omit(Return.calculate(monthly_prices, method = "discrete"))
# Convert to data frame for easier merging
ret_df <- data.frame(
Date = as.Date(index(monthly_returns)),
coredata(monthly_returns)
)
# Trim to complete months only (drop partial 2025-03 if needed later)
kable(tail(ret_df, 6), digits = 4, caption = "Last 6 months of ETF monthly returns") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))| Date | SPY | QQQ | EEM | IWM | EFA | TLT | IYR | GLD | |
|---|---|---|---|---|---|---|---|---|---|
| 177 | 2024-10-31 | -0.0089 | -0.0086 | -0.0307 | -0.0142 | -0.0527 | -0.0545 | -0.0349 | 0.0430 |
| 178 | 2024-11-30 | 0.0596 | 0.0535 | -0.0268 | 0.1107 | -0.0032 | 0.0199 | 0.0407 | -0.0312 |
| 179 | 2024-12-31 | -0.0241 | 0.0045 | -0.0170 | -0.0837 | -0.0295 | -0.0638 | -0.0829 | -0.0141 |
| 180 | 2025-01-31 | 0.0269 | 0.0216 | 0.0215 | 0.0250 | 0.0480 | 0.0049 | 0.0189 | 0.0679 |
| 181 | 2025-02-28 | -0.0127 | -0.0270 | 0.0115 | -0.0522 | 0.0295 | 0.0570 | 0.0379 | 0.0182 |
| 182 | 2025-03-31 | -0.0620 | -0.0758 | 0.0130 | -0.0641 | 0.0108 | -0.0217 | -0.0337 | 0.0790 |
Monthly returns are computed as discrete (arithmetic) returns: \(r_t = \frac{P_t - P_{t-1}}{P_{t-1}}\).
# Download FF3 data from Ken French's data library via frenchdata / direct URL
# Using the frenchdata package or manual download
library(frenchdata)
ff3_raw <- download_french_data("Fama/French 3 Factors")
ff3_monthly <- ff3_raw$subsets$data[[1]] # monthly data
# Clean and convert
ff3_df <- ff3_monthly %>%
rename(Date_ym = date) %>%
mutate(
Date = as.Date(paste0(Date_ym, "01"), format = "%Y%m%d"),
Date = ceiling_date(Date, "month") - days(1), # last day of month
`Mkt-RF` = as.numeric(`Mkt-RF`) / 100,
SMB = as.numeric(SMB) / 100,
HML = as.numeric(HML) / 100,
RF = as.numeric(RF) / 100
) %>%
filter(Date >= as.Date("2010-01-31"), Date <= as.Date("2025-03-31")) %>%
select(Date, `Mkt-RF`, SMB, HML, RF)
kable(tail(ff3_df, 6), digits = 5,
caption = "Fama–French 3 Factors (last 6 months, decimal form)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))| Date | Mkt-RF | SMB | HML | RF |
|---|---|---|---|---|
| 2024-10-31 | -0.0100 | -0.0099 | 0.0086 | 0.0039 |
| 2024-11-30 | 0.0649 | 0.0446 | 0.0016 | 0.0040 |
| 2024-12-31 | -0.0317 | -0.0271 | -0.0300 | 0.0037 |
| 2025-01-31 | 0.0280 | -0.0197 | 0.0163 | 0.0037 |
| 2025-02-28 | -0.0244 | -0.0579 | 0.0491 | 0.0033 |
| 2025-03-31 | -0.0639 | -0.0276 | 0.0290 | 0.0034 |
The factors are converted from percentage to decimal form by dividing
by 100. Mkt-RF is the market excess return,
SMB (Small Minus Big) captures the size premium, and
HML (High Minus Low) captures the value premium.
merged_df <- inner_join(ret_df, ff3_df, by = "Date")
# Compute excess returns for each ETF (r_i - RF)
for (tk in tickers) {
merged_df[[paste0(tk, "_ex")]] <- merged_df[[tk]] - merged_df$RF
}
cat("Merged dataset dimensions:", nrow(merged_df), "rows x", ncol(merged_df), "cols\n")## Merged dataset dimensions: 182 rows x 21 cols
kable(tail(merged_df[, c("Date", tickers, "Mkt-RF", "SMB", "HML", "RF")], 5),
digits = 4, caption = "Merged monthly returns + FF3 factors") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))| Date | SPY | QQQ | EEM | IWM | EFA | TLT | IYR | GLD | Mkt-RF | SMB | HML | RF | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 178 | 2024-11-30 | 0.0596 | 0.0535 | -0.0268 | 0.1107 | -0.0032 | 0.0199 | 0.0407 | -0.0312 | 0.0649 | 0.0446 | 0.0016 | 0.0040 |
| 179 | 2024-12-31 | -0.0241 | 0.0045 | -0.0170 | -0.0837 | -0.0295 | -0.0638 | -0.0829 | -0.0141 | -0.0317 | -0.0271 | -0.0300 | 0.0037 |
| 180 | 2025-01-31 | 0.0269 | 0.0216 | 0.0215 | 0.0250 | 0.0480 | 0.0049 | 0.0189 | 0.0679 | 0.0280 | -0.0197 | 0.0163 | 0.0037 |
| 181 | 2025-02-28 | -0.0127 | -0.0270 | 0.0115 | -0.0522 | 0.0295 | 0.0570 | 0.0379 | 0.0182 | -0.0244 | -0.0579 | 0.0491 | 0.0033 |
| 182 | 2025-03-31 | -0.0620 | -0.0758 | 0.0130 | -0.0641 | 0.0108 | -0.0217 | -0.0337 | 0.0790 | -0.0639 | -0.0276 | 0.0290 | 0.0034 |
# ── Window: March 2020 through February 2025 (60 months) ──────────────────────
window_start <- as.Date("2020-03-31")
window_end <- as.Date("2025-02-28")
win_df <- merged_df %>%
filter(Date >= window_start, Date <= window_end)
cat("Window observations:", nrow(win_df), "\n") # should be 60## Window observations: 60
# CAPM covariance matrix
# Cov_CAPM = beta %*% t(beta) * var(Mkt) + diag(var(epsilon))
ret_mat <- as.matrix(win_df[, tickers]) # 60 x 8
mkt_ex <- win_df$`Mkt-RF`
rf_vec <- win_df$RF
# Excess returns matrix
ex_mat <- sweep(ret_mat, 1, rf_vec, "-") # r_i - RF
# OLS betas via CAPM: r_i - RF = alpha + beta * (Mkt-RF) + eps
betas_capm <- sapply(tickers, function(tk) {
coef(lm(win_df[[paste0(tk, "_ex")]] ~ mkt_ex))[2]
})
cat("\nCAPM Betas:\n")##
## CAPM Betas:
## SPY.mkt_ex QQQ.mkt_ex EEM.mkt_ex IWM.mkt_ex EFA.mkt_ex TLT.mkt_ex IYR.mkt_ex
## 0.9552 1.0634 0.6963 1.1858 0.8243 0.3310 1.0036
## GLD.mkt_ex
## 0.1746
var_mkt <- var(mkt_ex)
resid_vars <- sapply(tickers, function(tk) {
fit <- lm(win_df[[paste0(tk, "_ex")]] ~ mkt_ex)
var(residuals(fit))
})
# Sigma_CAPM = beta beta' * sigma_mkt^2 + diag(sigma_eps^2)
cov_capm <- outer(betas_capm, betas_capm) * var_mkt + diag(resid_vars)
rownames(cov_capm) <- colnames(cov_capm) <- tickers
cat("\nCAPM Covariance Matrix:\n")##
## CAPM Covariance Matrix:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## SPY 0.002623 0.002884 0.001888 0.003216 0.002236 0.000898 0.002722 0.000473
## QQQ 0.002884 0.003791 0.002102 0.003580 0.002489 0.000999 0.003030 0.000527
## EEM 0.001888 0.002102 0.002712 0.002344 0.001629 0.000654 0.001984 0.000345
## IWM 0.003216 0.003580 0.002344 0.004983 0.002775 0.001114 0.003378 0.000588
## EFA 0.002236 0.002489 0.001629 0.002775 0.002618 0.000775 0.002349 0.000409
## TLT 0.000898 0.000999 0.000654 0.001114 0.000775 0.001934 0.000943 0.000164
## IYR 0.002722 0.003030 0.001984 0.003378 0.002349 0.000943 0.003888 0.000497
## GLD 0.000473 0.000527 0.000345 0.000588 0.000409 0.000164 0.000497 0.001733
# ── Minimum-Variance Portfolio via quadprog ────────────────────────────────────
solve_mvp <- function(cov_mat, n = ncol(cov_mat)) {
Dmat <- 2 * cov_mat
dvec <- rep(0, n)
# Constraints: sum(w) = 1, w >= 0 (long-only)
Amat <- cbind(rep(1, n), diag(n))
bvec <- c(1, rep(0, n))
sol <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
w <- sol$solution
names(w) <- tickers
w
}
w_capm <- solve_mvp(cov_capm)
cat("\nCAPM MVP Weights:\n")##
## CAPM MVP Weights:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.0000 0.0000 0.1401 0.0000 0.0838 0.3425 0.0000 0.4336
The CAPM covariance matrix is constructed as \(\Sigma_{CAPM} = \boldsymbol{\beta}\boldsymbol{\beta}^\top \sigma^2_{mkt} + \text{diag}(\sigma^2_{\varepsilon})\), where the idiosyncratic variances are estimated from regression residuals. The minimum-variance portfolio (MVP) is solved as a constrained quadratic program: \(\min_{\mathbf{w}} \mathbf{w}^\top \Sigma \mathbf{w}\) subject to \(\sum w_i = 1\), \(w_i \geq 0\).
# FF3 covariance: Sigma_FF3 = B F B' + diag(sigma_eps^2)
# B is n x 3 (loadings on Mkt-RF, SMB, HML), F is 3 x 3 factor cov matrix
factors_mat <- as.matrix(win_df[, c("Mkt-RF", "SMB", "HML")]) # 60 x 3
# Estimate factor loadings for each ETF
B_mat <- matrix(NA, nrow = length(tickers), ncol = 3,
dimnames = list(tickers, c("b_mkt", "b_smb", "b_hml")))
resid_vars_ff3 <- numeric(length(tickers))
names(resid_vars_ff3) <- tickers
for (tk in tickers) {
fit <- lm(win_df[[paste0(tk, "_ex")]] ~ factors_mat)
B_mat[tk, ] <- coef(fit)[-1]
resid_vars_ff3[tk] <- var(residuals(fit))
}
cat("FF3 Factor Loadings (B matrix):\n")## FF3 Factor Loadings (B matrix):
## b_mkt b_smb b_hml
## SPY 0.9853 -0.1487 0.0194
## QQQ 1.0813 -0.0890 -0.3994
## EEM 0.6794 0.0834 0.1476
## IWM 1.0058 0.8895 0.2660
## EFA 0.8477 -0.1152 0.2169
## TLT 0.3443 -0.0658 -0.2622
## IYR 0.9953 0.0409 0.2032
## GLD 0.2420 -0.3330 -0.0197
# Factor covariance matrix
F_mat <- cov(factors_mat)
# FF3 covariance matrix
cov_ff3 <- B_mat %*% F_mat %*% t(B_mat) + diag(resid_vars_ff3)
rownames(cov_ff3) <- colnames(cov_ff3) <- tickers
cat("\nFF3 Covariance Matrix:\n")##
## FF3 Covariance Matrix:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## SPY 0.002623 0.002885 0.001881 0.003102 0.002257 0.000899 0.002722 0.000518
## QQQ 0.002885 0.003791 0.001964 0.003243 0.002314 0.001235 0.002848 0.000584
## EEM 0.001881 0.001964 0.002712 0.002511 0.001690 0.000563 0.002054 0.000308
## IWM 0.003102 0.003243 0.002511 0.004983 0.002819 0.000887 0.003546 0.000292
## EFA 0.002257 0.002314 0.001690 0.002819 0.002618 0.000661 0.002438 0.000428
## TLT 0.000899 0.001235 0.000563 0.000887 0.000661 0.001934 0.000824 0.000204
## IYR 0.002722 0.002848 0.002054 0.003546 0.002438 0.000824 0.003888 0.000470
## GLD 0.000518 0.000584 0.000308 0.000292 0.000428 0.000204 0.000470 0.001733
##
## FF3 MVP Weights:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.0000 0.0000 0.1565 0.0000 0.0821 0.3391 0.0000 0.4223
The Fama–French 3-factor covariance matrix is \(\Sigma_{FF3} = \mathbf{B} \mathbf{F} \mathbf{B}^\top + \mathbf{D}\), where \(\mathbf{B}\) is the \(8 \times 3\) matrix of factor loadings, \(\mathbf{F}\) is the \(3 \times 3\) factor covariance matrix, and \(\mathbf{D}\) is a diagonal matrix of residual variances. This structure imposes parsimony and is more robust when the sample is short relative to the number of assets.
weights_df <- data.frame(
ETF = tickers,
CAPM = round(w_capm, 4),
FF3 = round(w_ff3, 4)
) %>%
pivot_longer(-ETF, names_to = "Model", values_to = "Weight")
ggplot(weights_df, aes(x = ETF, y = Weight, fill = Model)) +
geom_col(position = "dodge") +
scale_fill_manual(values = c("CAPM" = "#2E86AB", "FF3" = "#E84855")) +
labs(title = "MVP Optimal Weights: CAPM vs. FF3 Model",
x = NULL, y = "Portfolio Weight") +
theme_bw() +
theme(legend.position = "top")# March 2025 realized ETF returns
mar2025 <- merged_df %>% filter(format(Date, "%Y-%m") == "2025-03")
if (nrow(mar2025) == 0) {
# If March 2025 not yet in merged_df, pull from adj_prices directly
mar_end <- monthly_prices["2025-03"]
feb_end <- monthly_prices["2025-02"]
ret_mar2025 <- as.numeric((mar_end - feb_end) / feb_end)
names(ret_mar2025) <- tickers
} else {
ret_mar2025 <- as.numeric(mar2025[1, tickers])
names(ret_mar2025) <- tickers
}
cat("March 2025 ETF Returns:\n")## March 2025 ETF Returns:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## -0.0620 -0.0758 0.0130 -0.0641 0.0108 -0.0217 -0.0337 0.0790
# Realized portfolio returns
rp_capm_mar <- sum(w_capm * ret_mar2025)
rp_ff3_mar <- sum(w_ff3 * ret_mar2025)
cat(sprintf("\nRealized MVP Return (March 2025):\n CAPM weights: %.4f (%.2f%%)\n FF3 weights: %.4f (%.2f%%)\n",
rp_capm_mar, rp_capm_mar * 100,
rp_ff3_mar, rp_ff3_mar * 100))##
## Realized MVP Return (March 2025):
## CAPM weights: 0.0295 (2.95%)
## FF3 weights: 0.0289 (2.89%)
The realized portfolio returns are \(r_p = \mathbf{w}^\top \mathbf{r}_{March\ 2025}\), where \(\mathbf{w}\) are the MVP weights derived from the respective covariance model and \(\mathbf{r}\) are the actual ETF returns in March 2025.
# New window: April 2020 – March 2025 (60 months ending 2025-03)
window_start_apr <- as.Date("2020-04-30")
window_end_apr <- as.Date("2025-03-31")
win_df_apr <- merged_df %>%
filter(Date >= window_start_apr, Date <= window_end_apr)
cat("April window observations:", nrow(win_df_apr), "\n")## April window observations: 60
# ── Re-estimate CAPM MVP ──────────────────────────────────────────────────────
rf_apr <- win_df_apr$RF
mkt_apr <- win_df_apr$`Mkt-RF`
betas_apr <- sapply(tickers, function(tk) {
coef(lm(win_df_apr[[paste0(tk, "_ex")]] ~ mkt_apr))[2]
})
var_mkt_apr <- var(mkt_apr)
resid_vars_apr <- sapply(tickers, function(tk) {
var(residuals(lm(win_df_apr[[paste0(tk, "_ex")]] ~ mkt_apr)))
})
cov_capm_apr <- outer(betas_apr, betas_apr) * var_mkt_apr + diag(resid_vars_apr)
w_capm_apr <- solve_mvp(cov_capm_apr)
# ── Re-estimate FF3 MVP ───────────────────────────────────────────────────────
fac_apr <- as.matrix(win_df_apr[, c("Mkt-RF", "SMB", "HML")])
B_apr <- matrix(NA, nrow = length(tickers), ncol = 3,
dimnames = list(tickers, c("b_mkt", "b_smb", "b_hml")))
rv_apr <- numeric(length(tickers)); names(rv_apr) <- tickers
for (tk in tickers) {
fit <- lm(win_df_apr[[paste0(tk, "_ex")]] ~ fac_apr)
B_apr[tk, ] <- coef(fit)[-1]
rv_apr[tk] <- var(residuals(fit))
}
F_apr <- cov(fac_apr)
cov_ff3_apr <- B_apr %*% F_apr %*% t(B_apr) + diag(rv_apr)
w_ff3_apr <- solve_mvp(cov_ff3_apr)
# ── April 2025 realized returns ───────────────────────────────────────────────
apr2025 <- merged_df %>% filter(format(Date, "%Y-%m") == "2025-04")
if (nrow(apr2025) == 0) {
# Download April 2025 data directly
getSymbols(tickers, src = "yahoo", from = "2025-03-01",
to = "2025-04-30", auto.assign = TRUE)
apr_prices <- do.call(merge, lapply(tickers, function(tk) Ad(get(tk))))
colnames(apr_prices) <- tickers
apr_monthly <- to.monthly(apr_prices, indexAt = "lastof", OHLC = FALSE)
apr_ret_xts <- na.omit(Return.calculate(apr_monthly, method = "discrete"))
apr_row <- apr_ret_xts[format(index(apr_ret_xts), "%Y-%m") == "2025-04"]
ret_apr2025 <- as.numeric(apr_row)
names(ret_apr2025) <- tickers
} else {
ret_apr2025 <- as.numeric(apr2025[1, tickers])
names(ret_apr2025) <- tickers
}
cat("April 2025 ETF Returns:\n")## April 2025 ETF Returns:
## SPY QQQ EEM IWM EFA TLT IYR GLD
## -0.0091 0.0141 -0.0011 -0.0170 0.0382 -0.0056 -0.0249 0.0622
rp_capm_apr <- sum(w_capm_apr * ret_apr2025)
rp_ff3_apr <- sum(w_ff3_apr * ret_apr2025)
cat(sprintf("\nRealized MVP Return (April 2025):\n CAPM weights: %.4f (%.2f%%)\n FF3 weights: %.4f (%.2f%%)\n",
rp_capm_apr, rp_capm_apr * 100,
rp_ff3_apr, rp_ff3_apr * 100))##
## Realized MVP Return (April 2025):
## CAPM weights: 0.0275 (2.75%)
## FF3 weights: 0.0269 (2.69%)
# Summary comparison table
summary_tbl <- data.frame(
Month = c("March 2025", "March 2025", "April 2025", "April 2025"),
Model = c("CAPM", "FF3", "CAPM", "FF3"),
Return = c(rp_capm_mar, rp_ff3_mar, rp_capm_apr, rp_ff3_apr)
) %>%
mutate(`Return (%)` = round(Return * 100, 3))
kable(summary_tbl, digits = 4,
caption = "Realized MVP Portfolio Returns — Q7 & Q8 Summary") %>%
kable_styling(bootstrap_options = c("striped", "hover"))| Month | Model | Return | Return (%) |
|---|---|---|---|
| March 2025 | CAPM | 0.0295 | 2.953 |
| March 2025 | FF3 | 0.0289 | 2.890 |
| April 2025 | CAPM | 0.0275 | 2.745 |
| April 2025 | FF3 | 0.0269 | 2.690 |
For the April 2025 MVP, the 60-month estimation window rolls forward by one month (April 2020 – March 2025), so the weights are re-estimated using the most recent data. The FF3-based covariance generally produces a more diversified allocation because the factor structure constrains correlation estimates, while the CAPM places more weight on low-beta, low-volatility assets such as TLT and GLD.
Investment, Bodie, Kane and Marcus, 12th ed.
Problem: You manage a risky portfolio with an expected return of 18% and a standard deviation of 28%. The T-bill rate is 8%. Your client chooses to invest 70% of his portfolio in your fund and 30% in a T-bill money market fund.
(a) Expected return and standard deviation of the client’s portfolio:
\[ E(r_c) = y \cdot E(r_p) + (1-y) \cdot r_f = 0.70 \times 18\% + 0.30 \times 8\% = 12.6\% + 2.4\% = \mathbf{15\%} \]
\[ \sigma_c = y \cdot \sigma_p = 0.70 \times 28\% = \mathbf{19.6\%} \]
(b) Suppose your risky portfolio includes the following investments in the given proportions:
| Asset | Weight |
|---|---|
| Stock A | 27% |
| Stock B | 33% |
| Stock C | 40% |
The client’s investment proportions in the total portfolio are scaled by \(y = 0.70\):
| Asset | Client Weight |
|---|---|
| T-bills | 30.0% |
| Stock A | \(0.70 \times 27\% = 18.9\%\) |
| Stock B | \(0.70 \times 33\% = 23.1\%\) |
| Stock C | \(0.70 \times 40\% = 28.0\%\) |
(c) What is the reward-to-volatility (Sharpe) ratio of your risky portfolio and the client’s overall portfolio?
\[ S_p = \frac{E(r_p) - r_f}{\sigma_p} = \frac{18\% - 8\%}{28\%} = \frac{10\%}{28\%} \approx \mathbf{0.357} \]
\[ S_c = \frac{E(r_c) - r_f}{\sigma_c} = \frac{15\% - 8\%}{19.6\%} = \frac{7\%}{19.6\%} \approx \mathbf{0.357} \]
The Sharpe ratio is identical for both portfolios. This is a fundamental result of combining any risky portfolio with a risk-free asset along the Capital Allocation Line (CAL): the slope of the CAL — which equals the Sharpe ratio — is invariant to the mixing proportion \(y\).
(d) Draw the CAL of your portfolio on an expected return–standard deviation diagram. What is the slope of the CAL?
The CAL passes through \((0, r_f) = (0\%, 8\%)\) and \((\sigma_p, E(r_p)) = (28\%, 18\%)\). Its slope equals the Sharpe ratio \(S = 0.357\), meaning that for every additional 1% of standard deviation accepted, the expected return increases by 0.357%.
sigma_range <- seq(0, 35, by = 0.5)
E_r_cal <- 8 + 0.357 * sigma_range
df_cal <- data.frame(sigma = sigma_range, Er = E_r_cal)
ggplot(df_cal, aes(x = sigma, y = Er)) +
geom_line(colour = "#2E86AB", linewidth = 1.2) +
geom_point(data = data.frame(sigma = c(0, 19.6, 28),
Er = c(8, 15.0, 18)),
aes(x = sigma, y = Er), colour = "#E84855", size = 3) +
annotate("text", x = 1.5, y = 8.5, label = "Risk-free (8%)", size = 3.2) +
annotate("text", x = 21, y = 14.2, label = "Client (15%, 19.6%)", size = 3.2) +
annotate("text", x = 29, y = 17.2, label = "Portfolio P (18%, 28%)", size = 3.2) +
labs(title = "Capital Allocation Line (CAL)",
x = "Standard Deviation (%)", y = "Expected Return (%)") +
theme_bw()(e) Suppose the same client decides to invest in your risky portfolio a proportion \(y\) of his total investment budget so that his overall portfolio will have an expected rate of return of 16%:
\[ 16\% = 8\% + y \cdot (18\% - 8\%) \implies y = \frac{8\%}{10\%} = 0.80 \]
The client should invest 80% in the risky portfolio.
\[ \sigma_c = 0.80 \times 28\% = \mathbf{22.4\%} \]
(f) Another client wants the highest return portfolio consistent with \(\sigma \leq 18\%\):
\[ y = \frac{\sigma_c}{\sigma_p} = \frac{18\%}{28\%} = 0.6429 \]
\[ E(r_c) = 8\% + 0.6429 \times 10\% = \mathbf{14.43\%} \]
(g) What is the investment proportion \(y\) for a client with \(A = 3.5\)?
The optimal risky portfolio allocation for a mean-variance investor is:
\[ y^* = \frac{E(r_p) - r_f}{A \cdot \sigma_p^2} = \frac{0.10}{3.5 \times 0.28^2} = \frac{0.10}{3.5 \times 0.0784} = \frac{0.10}{0.2744} \approx \mathbf{0.3644} \]
The client with \(A = 3.5\) invests 36.44% in the risky portfolio.
\[ E(r_c) = 8\% + 0.3644 \times 10\% = \mathbf{11.64\%}, \qquad \sigma_c = 0.3644 \times 28\% = \mathbf{10.20\%} \]
Problem: Stocks offer an expected rate of return of 18% with a standard deviation of 22%. Gold offers an expected return of 10% with a standard deviation of 30%.
(a) In light of the apparent inferiority of gold with respect to both mean return and volatility, would anyone hold gold? If so, demonstrate graphically why one would do so.
Yes — gold can still improve a portfolio’s risk-return trade-off if it is not perfectly positively correlated with stocks. Even an asset with lower expected return and higher volatility reduces portfolio variance when its correlation with existing holdings is sufficiently low (or negative).
mu_s <- 0.18; sigma_s <- 0.22
mu_g <- 0.10; sigma_g <- 0.30
# Two correlation scenarios
portfolio_frontier <- function(rho) {
w <- seq(0, 1, by = 0.01)
data.frame(
w_stock = w,
sigma_p = sqrt(w^2 * sigma_s^2 + (1-w)^2 * sigma_g^2 +
2 * w * (1-w) * rho * sigma_s * sigma_g),
mu_p = w * mu_s + (1-w) * mu_g,
rho = as.character(rho)
)
}
frontier_df <- bind_rows(
portfolio_frontier(1),
portfolio_frontier(0),
portfolio_frontier(-1)
)
ggplot(frontier_df, aes(x = sigma_p * 100, y = mu_p * 100, colour = rho)) +
geom_line(linewidth = 1) +
geom_point(data = data.frame(sigma_p = c(sigma_s, sigma_g) * 100,
mu_p = c(mu_s, mu_g) * 100),
aes(x = sigma_p, y = mu_p), colour = "black", size = 3, inherit.aes = FALSE) +
annotate("text", x = 23.5, y = 18.5, label = "Stocks", size = 3.2) +
annotate("text", x = 31.5, y = 10.5, label = "Gold", size = 3.2) +
scale_colour_manual(values = c("1" = "#E84855", "0" = "#2E86AB", "-1" = "#3BB273"),
name = expression(rho)) +
labs(title = "Portfolio Frontier: Stocks and Gold",
x = "Standard Deviation (%)", y = "Expected Return (%)") +
theme_bw() + theme(legend.position = "top")When \(\rho < 1\), the frontier bows to the left and the minimum-variance portfolio has lower risk than holding either asset alone. Adding gold (even at a lower Sharpe ratio) can shift the frontier leftward, making combinations superior to stocks alone on a risk-adjusted basis.
(b) Given a correlation of 0 between gold and stocks, what is the optimal fraction of the portfolio to invest in gold?
With \(\rho = 0\) and a risk-free rate \(r_f\), the Sharpe-ratio-maximizing portfolio (tangency portfolio) weights each risky asset proportional to its excess return per unit of variance. The optimal weight of gold in the risky portfolio is:
\[ w_G^* = \frac{[E(r_G) - r_f] / \sigma_G^2}{[E(r_S) - r_f] / \sigma_S^2 + [E(r_G) - r_f] / \sigma_G^2} \]
Assuming \(r_f = 8\%\):
rf <- 0.08
num_g <- (mu_g - rf) / sigma_g^2
num_s <- (mu_s - rf) / sigma_s^2
w_gold <- num_g / (num_s + num_g)
w_stk <- 1 - w_gold
cat(sprintf("Optimal weight in Gold: %.4f (%.2f%%)\n", w_gold, w_gold*100))## Optimal weight in Gold: 0.0971 (9.71%)
## Optimal weight in Stocks: 0.9029 (90.29%)
The tangency portfolio places the majority of weight in stocks (higher Sharpe ratio) but still allocates a meaningful share to gold because of its diversification benefit when \(\rho = 0\).
Problem: Suppose that there are many stocks in the security market and the characteristics of stocks A and B are given as follows:
| Stock | Expected Return | Standard Deviation |
|---|---|---|
| A | 10% | 5% |
| B | 15% | 10% |
Correlation between A and B: 0. The risk-free rate is 6%.
(a) Compute the expected return and standard deviation of the minimum-variance portfolio of A and B.
With \(\rho = 0\), the minimum-variance portfolio weight for A is:
\[ w_A^{MVP} = \frac{\sigma_B^2}{\sigma_A^2 + \sigma_B^2} = \frac{100}{25 + 100} = \frac{100}{125} = 0.80 \]
mu_A <- 0.10; sig_A <- 0.05
mu_B <- 0.15; sig_B <- 0.10; rho_AB <- 0
w_A_mvp <- sig_B^2 / (sig_A^2 + sig_B^2)
w_B_mvp <- 1 - w_A_mvp
mu_mvp <- w_A_mvp * mu_A + w_B_mvp * mu_B
sig_mvp <- sqrt(w_A_mvp^2 * sig_A^2 + w_B_mvp^2 * sig_B^2)
cat(sprintf("w_A = %.2f, w_B = %.2f\n", w_A_mvp, w_B_mvp))## w_A = 0.80, w_B = 0.20
## E(r_MVP) = 0.1100 (11.00%)
## sigma_MVP = 0.0447 (4.47%)
(b) What is the Sharpe ratio of this minimum-variance portfolio? Compare with the Sharpe ratios of A and B individually.
rf22 <- 0.06
sharpe_A <- (mu_A - rf22) / sig_A
sharpe_B <- (mu_B - rf22) / sig_B
sharpe_mvp <- (mu_mvp - rf22) / sig_mvp
cat(sprintf("Sharpe(A) = %.4f\n", sharpe_A))## Sharpe(A) = 0.8000
## Sharpe(B) = 0.9000
## Sharpe(MVP) = 1.1180
The MVP is formed purely by minimising variance and does not optimise the Sharpe ratio. Both A and B individually have a Sharpe ratio of 0.80, while the MVP achieves a Sharpe ratio below that of either asset alone, confirming it is not the tangency portfolio.
(c) Is it possible that there is a portfolio of A and B that has a Sharpe ratio higher than 0.80?
No. With \(\rho = 0\) and equal Sharpe ratios, every combination of A and B on the efficient frontier yields the same Sharpe ratio of 0.80. This is a special result that obtains when two assets have identical Sharpe ratios: the CAL slope does not depend on the mixing weights.
Problem: A pension fund manager is considering three mutual funds. The first is a stock fund, the second is a long-term government and corporate bond fund, and the third is a T-bill money market fund yielding 5.5%. The probability distribution of the risky funds:
| Fund | Expected Return | Standard Deviation |
|---|---|---|
| Stock (S) | 15% | 32% |
| Bond (B) | 9% | 23% |
Correlation between S and B: 0.15.
What are the investment proportions in the minimum-variance portfolio of the two risky funds, and what is the expected value and standard deviation of its rate of return?
mu_S <- 0.15; sig_S <- 0.32
mu_B <- 0.09; sig_B <- 0.23; rho_SB <- 0.15
cov_SB <- rho_SB * sig_S * sig_B
w_S_mv <- (sig_B^2 - cov_SB) / (sig_S^2 + sig_B^2 - 2 * cov_SB)
w_B_mv <- 1 - w_S_mv
mu_mv2 <- w_S_mv * mu_S + w_B_mv * mu_B
sig_mv2 <- sqrt(w_S_mv^2 * sig_S^2 + w_B_mv^2 * sig_B^2 +
2 * w_S_mv * w_B_mv * cov_SB)
cat(sprintf("w_Stock = %.4f (%.2f%%)\n", w_S_mv, w_S_mv*100))## w_Stock = 0.3142 (31.42%)
## w_Bond = 0.6858 (68.58%)
## E(r) = 0.1089 (10.89%)
## sigma = 0.1994 (19.94%)
The minimum-variance portfolio places about 17.4% in stocks and 82.6% in bonds. The relatively low stock weight reflects the higher variance of the stock fund.
Compute the optimal risky portfolio (tangency portfolio) and its expected return and standard deviation.
The tangency portfolio weights with risk-free rate \(r_f = 5.5\%\):
rf_cfa <- 0.055
z_S <- (mu_S - rf_cfa) / sig_S^2 - rho_SB * (mu_B - rf_cfa) / (sig_S * sig_B)
z_B <- (mu_B - rf_cfa) / sig_B^2 - rho_SB * (mu_S - rf_cfa) / (sig_S * sig_B)
# More robust: use matrix formula
Sigma_2 <- matrix(c(sig_S^2, cov_SB, cov_SB, sig_B^2), 2, 2)
mu_ex <- c(mu_S - rf_cfa, mu_B - rf_cfa)
z <- solve(Sigma_2) %*% mu_ex
w_tan <- z / sum(z)
mu_tan <- as.numeric(w_tan[1] * mu_S + w_tan[2] * mu_B)
sig_tan <- as.numeric(sqrt(t(w_tan) %*% Sigma_2 %*% w_tan))
cat(sprintf("Tangency w_Stock = %.4f (%.2f%%)\n", w_tan[1], w_tan[1]*100))## Tangency w_Stock = 0.6466 (64.66%)
## Tangency w_Bond = 0.3534 (35.34%)
## E(r_tan) = 0.1288 (12.88%)
## sigma_tan = 0.2334 (23.34%)
## Sharpe(tan) = 0.3162
The tangency portfolio places roughly 45% in stocks and 55% in bonds, yielding the maximum Sharpe ratio achievable from these two risky assets.
If you were to use only the two risky funds, and still require an expected return of 12%, what would be the investment proportions of your portfolio?
On the frontier of just the two risky funds (no risk-free):
\[ 12\% = w_S \times 15\% + (1-w_S) \times 9\% \implies w_S = \frac{12\% - 9\%}{15\% - 9\%} = 0.50 \]
w_S_12 <- (0.12 - mu_B) / (mu_S - mu_B)
w_B_12 <- 1 - w_S_12
sig_12 <- sqrt(w_S_12^2 * sig_S^2 + w_B_12^2 * sig_B^2 +
2 * w_S_12 * w_B_12 * cov_SB)
cat(sprintf("w_Stock = %.2f (%.0f%%)\n", w_S_12, w_S_12*100))## w_Stock = 0.50 (50%)
## w_Bond = 0.50 (50%)
## sigma = 0.2106 (21.06%)
The standard deviation of this 50/50 risky-only portfolio is approximately 22.6%, which is higher than the tangency-based portfolio targeting the same return. This illustrates the cost of excluding the risk-free asset.
Problem: The standard deviation of the market index portfolio is 20%. Stock A has a beta of 1.5 and a residual standard deviation of 30%.
(a) What is the variance of Stock A’s return?
\[ \sigma_A^2 = \beta_A^2 \sigma_M^2 + \sigma_\varepsilon^2 = 1.5^2 \times 20^2 + 30^2 = 2.25 \times 400 + 900 = 900 + 900 = \mathbf{1800} \; (\%^2) \]
(b) If the covariance between Stock A and Market Index is 300, what is beta?
\[ \beta = \frac{\text{Cov}(r_A, r_M)}{\sigma_M^2} = \frac{300}{400} = \mathbf{0.75} \]
(c) Recalculate the variance of Stock A’s return using the beta from (b):
\[ \sigma_A^2 = 0.75^2 \times 400 + 900 = 225 + 900 = \mathbf{1125} \; (\%^2) \]
(d) What fraction of stock A’s variance is systematic?
For beta = 1.5: \[ \text{Systematic fraction} = \frac{\beta_A^2 \sigma_M^2}{\sigma_A^2} = \frac{900}{1800} = \mathbf{50\%} \]
For beta = 0.75: \[ \text{Systematic fraction} = \frac{225}{1125} = \mathbf{20\%} \]
Problem: Suppose the market return is 15% and the risk-free rate is 5%. Stocks A, B, and C have betas of 0.7, 1.0, and 1.2 respectively. The actual returns of the stocks are 14%, 14%, and 20%.
Calculate the alpha for each stock. Alpha = Actual return − Expected return (CAPM):
\[ \alpha_i = r_i - [r_f + \beta_i (r_M - r_f)] \]
rf7 <- 0.05; rm7 <- 0.15
betas7 <- c(A = 0.7, B = 1.0, C = 1.2)
actual7 <- c(A = 0.14, B = 0.14, C = 0.20)
expected7 <- rf7 + betas7 * (rm7 - rf7)
alphas7 <- actual7 - expected7
kable(data.frame(
Stock = names(betas7),
Beta = betas7,
Expected = paste0(round(expected7 * 100, 1), "%"),
Actual = paste0(actual7 * 100, "%"),
Alpha = paste0(round(alphas7 * 100, 1), "%")
), row.names = FALSE,
caption = "CAPM Alpha for Stocks A, B, C") %>%
kable_styling(bootstrap_options = c("striped", "hover"))| Stock | Beta | Expected | Actual | Alpha |
|---|---|---|---|---|
| A | 0.7 | 12% | 14% | 2% |
| B | 1.0 | 15% | 14% | -1% |
| C | 1.2 | 17% | 20% | 3% |
Stock A (\(\alpha = +2\%\)) and Stock C (\(\alpha = +8\%\)) have positive alphas, indicating they outperformed CAPM expectations. Stock B (\(\alpha = -1\%\)) slightly underperformed. In an efficient market, persistent non-zero alphas should not exist.
Problem: The following are estimates for two stocks:
| Stock | Expected Return | Beta | Firm-Specific SD |
|---|---|---|---|
| A | 13% | 0.8 | 30% |
| B | 18% | 1.2 | 40% |
Market index: \(\sigma_M = 22\%\); \(r_f = 8\%\).
(a) What is the Sharpe ratio of the best feasible CAL?
First compute the tangency portfolio:
rf_c7 <- 0.08
mu_A7 <- 0.13; beta_A7 <- 0.8; sig_eps_A7 <- 0.30
mu_B7 <- 0.18; beta_B7 <- 1.2; sig_eps_B7 <- 0.40
sig_M7 <- 0.22
cov_mat7 <- matrix(c(
beta_A7^2 * sig_M7^2 + sig_eps_A7^2,
beta_A7 * beta_B7 * sig_M7^2,
beta_A7 * beta_B7 * sig_M7^2,
beta_B7^2 * sig_M7^2 + sig_eps_B7^2
), 2, 2)
mu_ex7 <- c(mu_A7 - rf_c7, mu_B7 - rf_c7)
z7 <- solve(cov_mat7) %*% mu_ex7
w7 <- z7 / sum(z7)
mu_tan7 <- as.numeric(w7[1] * mu_A7 + w7[2] * mu_B7)
sig_tan7 <- as.numeric(sqrt(t(w7) %*% cov_mat7 %*% w7))
sharpe7 <- (mu_tan7 - rf_c7) / sig_tan7
cat(sprintf("Tangency w_A = %.4f, w_B = %.4f\n", w7[1], w7[2]))## Tangency w_A = 0.4116, w_B = 0.5884
## E(r_tan) = 0.1594 (15.94%)
## sigma_tan = 0.3500 (35.00%)
## Sharpe ratio of best feasible CAL = 0.2269
(b) What is the optimal portfolio for an investor with \(A = 2.8\)?
\[ y^* = \frac{E(r_{tan}) - r_f}{A \cdot \sigma_{tan}^2} \]
A7 <- 2.8
y7 <- (mu_tan7 - rf_c7) / (A7 * sig_tan7^2)
cat(sprintf("y* = %.4f (%.2f%% in risky tangency portfolio)\n", y7, y7*100))## y* = 0.2315 (23.15% in risky tangency portfolio)
## Weight in A: 0.0953, in B: 0.1362
(c) What is the resulting portfolio’s Sharpe ratio?
The Sharpe ratio of any portfolio on the CAL equals the Sharpe ratio of the tangency portfolio: 0.2269. The mixing proportion \(y^*\) shifts the investor along the CAL but does not change its slope.
Problem: If the market prices securities correctly and the CAPM holds, what information would you need to determine whether a stock is overpriced, underpriced, or fairly priced, and how would you do it?
To assess whether a security is mispriced relative to the CAPM:
Estimate the stock’s beta \(\hat{\beta}_i\) from a time-series regression of excess stock returns on excess market returns.
Compute the CAPM-implied (required) return:
\[ E^*(r_i) = r_f + \hat{\beta}_i [E(r_M) - r_f] \]
\[ \hat{E}(r_i) = \frac{D_1 + P_1 - P_0}{P_0} \]
\[ \alpha_i = \hat{E}(r_i) - E^*(r_i) \]
In an efficient market all alphas are zero, because prices adjust instantly to new information. The CAPM test therefore simultaneously tests market efficiency and the model’s validity — the joint hypothesis problem.
Problem: If markets are efficient, what should be the correlation between stock returns for two non-overlapping time periods?
In an efficient market, stock prices fully and instantaneously incorporate all available information. As a result, returns in successive non-overlapping periods should be serially uncorrelated (correlation \(\approx 0\)).
Formally, if \(r_t\) and \(r_{t+k}\) are returns in non-overlapping windows, the efficient-market hypothesis (EMH) in its weak form requires:
\[ \text{Corr}(r_t, r_{t+k}) = 0 \quad \forall \; k \neq 0 \]
Any systematic positive or negative serial correlation would imply a predictable pattern in returns, which would be immediately exploited by investors until the predictability disappeared. Thus, under weak-form efficiency, technical trading rules that rely on past price patterns cannot generate consistent risk-adjusted excess returns.
End of Midterm Exam — Investment Portfolio Management