Part I:

1: Download ETF Data

tickers <- c("SPY","QQQ","EEM","IWM","EFA","TLT","IYR","GLD")

getSymbols(tickers, from = "2010-01-01", to = "2025-04-30")
## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"

Extract adjusted prices:

prices <- map(tickers, ~ Ad(get(.x))) %>%
  reduce(merge)

colnames(prices) <- tickers

2: Monthly Returns

monthly_prices <- to.monthly(prices, indexAt = "lastof", OHLC = FALSE)

monthly_returns <- data.frame(
  Date = index(monthly_prices),
  coredata(monthly_prices)
) %>%
  arrange(Date) %>%
  mutate(across(-Date, ~ . / lag(.) - 1)) %>%
  drop_na()

head(monthly_returns)
##         Date         SPY         QQQ          EEM         IWM          EFA
## 1 2010-02-28  0.03119480  0.04603899  0.017763847  0.04475121  0.002667385
## 2 2010-03-31  0.06088004  0.07710885  0.081108933  0.08230709  0.063854086
## 3 2010-04-30  0.01546980  0.02242545 -0.001661812  0.05678469 -0.028045778
## 4 2010-05-31 -0.07945465 -0.07392399 -0.093936195 -0.07536629 -0.111927677
## 5 2010-06-30 -0.05174074 -0.05975649 -0.013986011 -0.07743434 -0.020619474
## 6 2010-07-31  0.06830026  0.07258228  0.109324702  0.06730928  0.116103906
##            TLT         IYR          GLD
## 1 -0.003423707  0.05457010  0.032748219
## 2 -0.020574803  0.09748474 -0.004386396
## 3  0.033219065  0.06388149  0.058834363
## 4  0.051083188 -0.05683540  0.030513147
## 5  0.057978961 -0.04670129  0.023553189
## 6 -0.009463873  0.09404813 -0.050871157

3: Fama-French 3 Factors

url <- "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip"

temp <- tempfile()
download.file(url, temp)

ff_raw <- read_csv(unz(temp, "F-F_Research_Data_Factors.csv"), skip = 3)
## New names:
## • `` -> `...1`
## Warning: One or more parsing issues, call `problems()` on your data frame for details,
## e.g.:
##   dat <- vroom(...)
##   problems(dat)
## Rows: 1298 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): ...1
## dbl (4): Mkt-RF, SMB, HML, RF
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Clean + align to END OF MONTH:

ff <- ff_raw %>%
  rename(Date = ...1) %>%
  filter(!is.na(Date)) %>%
  filter(Date >= 201001 & Date <= 202504) %>%
  mutate(across(-Date, as.numeric)) %>%
  mutate(across(-Date, ~ . / 100)) %>%
  mutate(Date = ymd(paste0(Date, "01"))) %>%
  mutate(Date = ceiling_date(Date, "month") - days(1))  # 🔥 CRITICAL
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `Date = ymd(paste0(Date, "01"))`.
## Caused by warning:
## !  13 failed to parse.
head(ff)
## # A tibble: 6 × 5
##   Date       `Mkt-RF`     SMB     HML     RF
##   <date>        <dbl>   <dbl>   <dbl>  <dbl>
## 1 2010-01-31  -0.0335  0.0043  0.0033 0     
## 2 2010-02-28   0.0339  0.0118  0.0318 0     
## 3 2010-03-31   0.063   0.0146  0.0219 0.0001
## 4 2010-04-30   0.0199  0.0484  0.0296 0.0001
## 5 2010-05-31  -0.079   0.0013 -0.0248 0.0001
## 6 2010-06-30  -0.0556 -0.0179 -0.0473 0.0001

4: Merge Data

data_all <- left_join(monthly_returns, ff, by = "Date") %>%
  drop_na()

head(data_all)
##         Date         SPY         QQQ          EEM         IWM          EFA
## 1 2010-02-28  0.03119480  0.04603899  0.017763847  0.04475121  0.002667385
## 2 2010-03-31  0.06088004  0.07710885  0.081108933  0.08230709  0.063854086
## 3 2010-04-30  0.01546980  0.02242545 -0.001661812  0.05678469 -0.028045778
## 4 2010-05-31 -0.07945465 -0.07392399 -0.093936195 -0.07536629 -0.111927677
## 5 2010-06-30 -0.05174074 -0.05975649 -0.013986011 -0.07743434 -0.020619474
## 6 2010-07-31  0.06830026  0.07258228  0.109324702  0.06730928  0.116103906
##            TLT         IYR          GLD  Mkt-RF     SMB     HML    RF
## 1 -0.003423707  0.05457010  0.032748219  0.0339  0.0118  0.0318 0e+00
## 2 -0.020574803  0.09748474 -0.004386396  0.0630  0.0146  0.0219 1e-04
## 3  0.033219065  0.06388149  0.058834363  0.0199  0.0484  0.0296 1e-04
## 4  0.051083188 -0.05683540  0.030513147 -0.0790  0.0013 -0.0248 1e-04
## 5  0.057978961 -0.04670129  0.023553189 -0.0556 -0.0179 -0.0473 1e-04
## 6 -0.009463873  0.09404813 -0.050871157  0.0692  0.0022 -0.0050 1e-04

5: MVP (Sample Covariance)

window_data <- data_all %>%
  filter(Date >= as.Date("2020-03-31") & Date <= as.Date("2025-02-28"))

nrow(window_data)   # should be ~60
## [1] 62
R <- as.matrix(window_data[, tickers])

Sigma <- cov(R)

one <- rep(1, ncol(R))

w_mvp <- solve(Sigma) %*% one / as.numeric(t(one) %*% solve(Sigma) %*% one)

w_mvp
##            [,1]
## SPY  1.50933050
## QQQ -0.72082244
## EEM  0.08842939
## IWM  0.01256008
## EFA -0.20232253
## TLT  0.52190289
## IYR -0.52336094
## GLD  0.31428304

6: MVP using FF 3-Factor Model

Estimate Betas Safely

betas <- map_dfr(tickers, function(tk) {

  df <- window_data %>%
    select(all_of(tk), `Mkt-RF`, SMB, HML) %>%
    drop_na()

  model <- lm(df[[1]] ~ ., data = df[, -1])

  tibble(asset = tk, t(coef(model)[-1]))
})

betas
## # A tibble: 8 × 2
##   asset `t(coef(model)[-1])`[,1]    [,2]    [,3]
##   <chr>                    <dbl>   <dbl>   <dbl>
## 1 SPY                      0.890 -0.151  -0.118 
## 2 QQQ                      1.01  -0.0883 -0.497 
## 3 EEM                      0.654  0.0176  0.0532
## 4 IWM                      0.963  0.734   0.0644
## 5 EFA                      0.778 -0.179   0.0604
## 6 TLT                      0.316 -0.0511 -0.286 
## 7 IYR                      0.883  0.0433  0.0461
## 8 GLD                      0.250 -0.288   0.0365

Factor Covariance

Sigma_f <- cov(window_data %>% select(`Mkt-RF`, SMB, HML))

Asset Covariance via FF Model

B <- as.matrix(betas[, -1])

Sigma_ff <- B %*% Sigma_f %*% t(B)

# Regularization (prevents singular matrix)
Sigma_ff <- Sigma_ff + diag(1e-6, nrow(Sigma_ff))

Compute MVP Weights

one <- rep(1, ncol(Sigma_ff))

w_mvp_ff <- solve(Sigma_ff) %*% one / as.numeric(t(one) %*% solve(Sigma_ff) %*% one)

w_mvp_ff
##            [,1]
## [1,] -0.2301029
## [2,] -0.3050903
## [3,]  0.2070702
## [4,]  0.1900879
## [5,] -0.1103451
## [6,]  0.7426298
## [7,] -0.1263123
## [8,]  0.6320626

7: Realized Return (March 2025)

march <- data_all %>% filter(Date == as.Date("2025-03-31"))

r_march <- as.numeric(march[, tickers])

ret_capm <- sum(w_mvp * r_march)
ret_ff   <- sum(w_mvp_ff * r_march)

ret_capm
## [1] 0.005993276
ret_ff
## [1] 0.0787977

8: April 2025 (Rolling Window)

window_april <- data_all %>%
  filter(Date >= as.Date("2020-04-30") & Date <= as.Date("2025-03-31"))

R2 <- as.matrix(window_april[, tickers])

Sigma2 <- cov(R2)

one <- rep(1, ncol(R2))

w_mvp2 <- solve(Sigma2) %*% one / as.numeric(t(one) %*% solve(Sigma2) %*% one)
april <- data_all %>% filter(Date == as.Date("2025-04-30"))

r_april <- as.numeric(april[, tickers])

ret_april <- sum(w_mvp2 * r_april)

ret_april
## [1] -0.00165042

Part II: Textbook Problem Sets

[cite_start]This section provides the solutions to the assigned problems from Investments (12th ed.)[cite: 16, 23].


Chapter 5: Risk, Return, and the Historical Record

Problem Set 12

Question: Calculating the risk premium and the real rate of return. Solution: 1. Real Rate of Return: Using the Fisher equation: \[r \approx R - i\] or the exact formula: \[1 + r = \frac{1 + R}{1 + i}\] Where \(R\) is the nominal rate and \(i\) is inflation. 2. Risk Premium: This is the difference between the expected HPR and the risk-free rate (\(E(r) - r_f\)).


Chapter 6: Capital Allocation to Risky Assets

Problem Set 21 and 22

Solution: * 21: This typically asks for the composition of the risky portfolio and the client’s overall portfolio. Calculate the proportion \(y\) using: \[y = \frac{E(r_p) - r_f}{A \sigma_p^2}\] * 22: Calculate the new expected return and standard deviation based on the shift in the Capital Allocation Line (CAL).

CFA Problems: 4, 5, and 8

  • Problem 4: Selection of the optimal portfolio depends on the investor’s risk aversion coefficient (\(A\)).
  • Problem 5: Identify the point of tangency between the indifference curve and the CAL.
  • Problem 8: Focuses on the “Separation Property”—the determination of the optimal risky portfolio is independent of personal risk preferences.

Chapter 7: Optimal Risky Portfolios

Problem Set 11 and 12

Solution: * 11: Requires finding the weights in a two-asset portfolio that minimize variance (the Minimum Variance Portfolio). * 12: Calculate the expected return and standard deviation of a portfolio with a given correlation coefficient (\(\rho\)). Note that diversification benefits are maximized when \(\rho = -1\).

CFA Problem 12

  • Solution: This involves the Efficient Frontier. An investor will choose a portfolio on the frontier that provides the highest utility based on their specific risk tolerance.

Chapter 8: Index Models

Problem Set 17

Question: Comparing the Single-Index Model (SIM) to the Markowitz Model. Solution: * The number of estimates required for the Markowitz model is \(n(n+3)/2\). * The SIM simplifies this significantly by only requiring \(3n + 2\) estimates. Calculate the difference for the number of assets given in the problem.

CFA Problem 1

Solution: Usually involves identifying systematic vs. firm-specific risk. * Systematic Risk: \(\beta_i^2 \sigma_m^2\) * Firm-specific (Residual) Risk: \(\sigma^2(e_i)\)