Part I — Computer Questions (40%)

Q1. Download ETF Daily Prices from Yahoo Finance (2010–2025)

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
tail(adj_prices)
##                 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.

Q2. Calculate Monthly Discrete Returns

# 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"))
Last 6 months of ETF monthly returns
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}}\).

Q3. Download Fama–French 3-Factor Monthly Data

# 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"))
Fama–French 3 Factors (last 6 months, decimal form)
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.

Q4. Merge Monthly ETF Returns and FF3 Factors

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"))
Merged monthly returns + FF3 factors
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

Q5. CAPM-Based MVP — Covariance from Past 60 Months (2020/03 – 2025/02)

# ── 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:
print(round(betas_capm, 4))
## 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:
print(round(cov_capm, 6))
##          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:
print(round(w_capm, 4))
##    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\).

Q6. FF3-Based MVP — Covariance from Past 60 Months (2020/03 – 2025/02)

# 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):
print(round(B_mat, 4))
##      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:
print(round(cov_ff3, 6))
##          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
w_ff3 <- solve_mvp(cov_ff3)

cat("\nFF3 MVP Weights:\n")
## 
## FF3 MVP Weights:
print(round(w_ff3, 4))
##    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")

Q7. Realized Portfolio Returns in March 2025

# 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:
print(round(ret_mar2025, 4))
##     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.

Q8. Realized MVP Return in April 2025 (Rolling 60-Month Window)

# 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:
print(round(ret_apr2025, 4))
##     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"))
Realized MVP Portfolio Returns — Q7 & Q8 Summary
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.


Part II — Textbook Questions (60%)

Investment, Bodie, Kane and Marcus, 12th ed.


Chapter 5

Problem 12

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\%} \]


Chapter 6

Problem 21

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%)
cat(sprintf("Optimal weight in Stocks: %.4f (%.2f%%)\n", w_stk,  w_stk*100))
## 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 22

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
cat(sprintf("E(r_MVP) = %.4f (%.2f%%)\n", mu_mvp, mu_mvp*100))
## E(r_MVP) = 0.1100 (11.00%)
cat(sprintf("sigma_MVP = %.4f (%.2f%%)\n", sig_mvp, sig_mvp*100))
## 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
cat(sprintf("Sharpe(B)   = %.4f\n", sharpe_B))
## Sharpe(B)   = 0.9000
cat(sprintf("Sharpe(MVP) = %.4f\n", sharpe_mvp))
## 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.

CFA Problem 4

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%)
cat(sprintf("w_Bond  = %.4f (%.2f%%)\n", w_B_mv, w_B_mv*100))
## w_Bond  = 0.6858 (68.58%)
cat(sprintf("E(r)    = %.4f (%.2f%%)\n", mu_mv2, mu_mv2*100))
## E(r)    = 0.1089 (10.89%)
cat(sprintf("sigma   = %.4f (%.2f%%)\n", sig_mv2, sig_mv2*100))
## 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.

CFA Problem 5

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%)
cat(sprintf("Tangency w_Bond  = %.4f (%.2f%%)\n", w_tan[2], w_tan[2]*100))
## Tangency w_Bond  = 0.3534 (35.34%)
cat(sprintf("E(r_tan) = %.4f (%.2f%%)\n", mu_tan, mu_tan*100))
## E(r_tan) = 0.1288 (12.88%)
cat(sprintf("sigma_tan = %.4f (%.2f%%)\n", sig_tan, sig_tan*100))
## sigma_tan = 0.2334 (23.34%)
cat(sprintf("Sharpe(tan) = %.4f\n", (mu_tan - rf_cfa) / sig_tan))
## 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.

CFA Problem 8

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%)
cat(sprintf("w_Bond  = %.2f (%.0f%%)\n", w_B_12, w_B_12*100))
## w_Bond  = 0.50 (50%)
cat(sprintf("sigma   = %.4f (%.2f%%)\n", sig_12, sig_12*100))
## 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.


Chapter 7

Problem 11

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 12

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"))
CAPM Alpha for Stocks A, B, C
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.

CFA Problem 12

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
cat(sprintf("E(r_tan) = %.4f (%.2f%%)\n", mu_tan7, mu_tan7*100))
## E(r_tan) = 0.1594 (15.94%)
cat(sprintf("sigma_tan = %.4f (%.2f%%)\n", sig_tan7, sig_tan7*100))
## sigma_tan = 0.3500 (35.00%)
cat(sprintf("Sharpe ratio of best feasible CAL = %.4f\n", sharpe7))
## 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)
cat(sprintf("Weight in A: %.4f, in B: %.4f\n", y7*w7[1], y7*w7[2]))
## 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.


Chapter 8

Problem 17

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:

  1. Estimate the stock’s beta \(\hat{\beta}_i\) from a time-series regression of excess stock returns on excess market returns.

  2. Compute the CAPM-implied (required) return:

\[ E^*(r_i) = r_f + \hat{\beta}_i [E(r_M) - r_f] \]

  1. Estimate the stock’s expected return from analyst forecasts or dividend discount models:

\[ \hat{E}(r_i) = \frac{D_1 + P_1 - P_0}{P_0} \]

  1. Compute alpha:

\[ \alpha_i = \hat{E}(r_i) - E^*(r_i) \]

  • \(\alpha_i > 0\): stock is underpriced (expected return exceeds required return; buy).
  • \(\alpha_i < 0\): stock is overpriced (expected return falls short of required return; sell).
  • \(\alpha_i = 0\): stock is fairly priced.

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.

CFA Problem 1

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