Part 1: Questions from Textbook (60%)

Chapter 7

1. a. Will the limitation to 20 stocks likely increase or decrease the risk of the portfolio? Explain. Limiting the portfolio to 20 stocks instead of the historical 40 stocks will likely increase the portfolio’s specific (unsystematic) risk. Because Hennessy focuses on individual stock selection rather than market timing or industry weighting, reducing the number of holdings decreases the diversification benefits and makes the portfolio’s total return more sensitive to the idiosyncratic shocks of individual companies.

1. b. Is there any way Hennessy could reduce the number of issues from 40 to 20 without significantly affecting risk? Explain. Yes, Hennessy could theoretically reduce the issues to 20 without significantly affecting risk by ensuring the new 20-stock portfolio maintains the exact same aggregate factor exposures (such as market beta and industry weightings) as the original 40-stock portfolio, and by deliberately choosing stocks with sufficiently low correlations to one another to preserve the diversification effect.

2. Explain why reduction to 10 might be less likely to be advantageous. While dropping from 40 to 20 stocks sacrifices some diversification, dropping further to 10 stocks creates a severely concentrated portfolio. The marginal risk-reduction benefits of diversification diminish rapidly as you add stocks, meaning a 10-stock portfolio exposes the fund to massive specific risk where idiosyncratic events affecting a single stock would overwhelmingly dictate the total portfolio variance.

3. Explain how this broader point of view could affect the committee decision to limit the holdings in the Hennessy portfolio to either 10 or 20 issues. The five other managers at Wilstead Pension Fund manage an aggregate of $250 million across more than 150 individual issues. Because this overarching structure is highly diversified and will easily absorb Hennessy’s unsystematic risk, evaluating Hennessy’s portfolio independently is flawed. From a total-fund perspective, allowing Hennessy to manage a highly concentrated 10- or 20-stock portfolio of its absolute “best ideas” is a sound strategy to maximize the manager’s demonstrated stock-picking skills without compromising the total pension fund’s risk profile.

4. Which one of the following portfolios cannot lie on the efficient frontier as described by Markowitz? Portfolio Y cannot lie on the efficient frontier. When compared to Portfolio X, Portfolio Y offers a lower expected return (9% vs. 12%) but carries a higher risk, with a standard deviation of 21% vs. 15%. Because Portfolio X completely dominates it, Portfolio Y is inefficient.

10. Which portfolio would you recommend? I recommend the portfolio made up of equal amounts of stocks B and C. Both portfolios pair a high-risk asset with a 40% standard deviation (A or C) and an asset with a 20% standard deviation (B). However, the correlation between B and C is just 0.10, whereas the correlation between A and B is 0.90. The significantly lower correlation in the B&C portfolio provides superior diversification, resulting in a lower total portfolio standard deviation.


Chapter 8

1. Explain what these regression results tell the analyst about risk-return relationships… Comment on their implications for future risk-return relationships… The regression results over the 5-year sample period reveal that ABC had negative risk-adjusted performance with an Alpha of -3.20%, while XYZ outperformed with an Alpha of 7.3%. ABC is a defensive stock (Beta = 0.60), whereas XYZ is more aggressive (Beta = 0.97). A larger proportion of ABC’s variance is explained by the market (R² = 0.35 vs. 0.17), which explains why XYZ carries substantially more specific risk (residual standard deviation of 21.45% vs. 13.02%). For future implications, the conflicting beta estimates from Brokerage Houses A and B (e.g., ABC beta of 0.62 vs 0.71) highlight parameter instability. Since historical betas vary depending on the sample period and observation frequency, investors must exercise caution when applying historical systematic risk metrics to forecast future relationships.

2. What percentage of Baker Fund’s total risk is specific (i.e., nonsystematic)? Given a correlation coefficient of 0.70, the percentage of systematic risk is measured by R², which is \(0.70^2 = 0.49\) (or 49%). Therefore, the percentage of specific (nonsystematic) risk is \(1 - 0.49 = 0.51\). 51% of the total risk is specific.

3. Based on this analysis, what is the implied beta of Charlottesville International? With a correlation of 1.0, the fund moves perfectly in tandem with the market index, carrying no unsystematic risk. Using the CAPM framework: Expected Return = Risk-Free Rate + Beta × (Market Return - Risk-Free Rate). \(9\% = 3\% + Beta \times (11\% - 3\%)\) \(6\% = Beta \times 8\%\) Beta = 0.75.

4. The concept of beta is most closely associated with: d. Systematic risk.

5. Beta and standard deviation differ as risk measures in that beta measures: b. Only systematic risk, while standard deviation measures total risk.


Chapter 9

8. When plotting portfolio R in the preceding table relative to the SML, portfolio R lies: d. Insufficient data given. To plot Portfolio R relative to the Security Market Line (SML), we require the risk-free rate to establish the SML’s intercept. This rate is absent from the provided table.

9. When plotting portfolio R relative to the capital market line, portfolio R lies: b. Below the CML. The Capital Market Line contains only optimally diversified portfolios perfectly correlated with the market (\(\rho = 1.0\)). We can deduce Portfolio R’s correlation as \(\rho = Beta \times (Standard Deviation_M / Standard Deviation_R)\). Using the table data, \(0.5 \times (12\% / 10\%) = 0.6\). Because the correlation is less than 1, it carries unsystematic risk and falls below the CML.

10. Briefly explain whether investors should expect a higher return on portfolio A than on portfolio B according to the capital asset pricing model. No, investors should not expect a higher return. Under the CAPM framework, expected return is strictly a function of systematic risk (beta). Because both Portfolios A and B have identical betas of 1.0, their expected returns are equal. Specific risk is diversifiable, and the market does not reward it with a higher expected return.


Chapter 10

13. According to the APT, if the risk-free rate is 4%, what should be McCracken’s estimate of the expected return of Orb’s High Growth Fund? Under APT, expected return is the risk-free rate plus the sum of factor sensitivities multiplied by their respective premiums. The factor premiums are 8% for GDP and 2% for inflation. The High Growth Fund’s sensitivities are 1.25 and 1.5 respectively. \(E(R) = 4\% + 1.25(8\%) + 1.5(2\%) = 4\% + 10\% + 3\% = 17\%\). The expected return is 17%.

14. With respect to McCracken’s APT model estimate of Orb’s Large Cap Fund and the information Kwon provides, is an arbitrage opportunity available? No, there is no arbitrage opportunity. Kwon estimates an expected risk premium of 8.5% above the risk-free rate. Using McCracken’s APT sensitivities for the Large Cap Fund (0.75 for GDP, 1.25 for inflation), the expected risk premium is: \(0.75(8\%) + 1.25(2\%) = 6\% + 2.5\% = 8.5\%\). Because the APT required premium exactly matches fundamental expectations, the fund is fairly priced.

15. If the GDP Fund is constructed from the other three funds, which of the following would be its weight in the Utility Fund? (a) -2.2. To create a portfolio with a unit sensitivity to GDP and 0 to inflation, weights must sum to 1.

  1. \(w_H + w_L + w_U = 1\)
  2. \(1.25w_H + 0.75w_L + 1.0w_U = 1\) (GDP sensitivity)
  3. \(1.5w_H + 1.25w_L + 2.0w_U = 0\) (Inflation sensitivity) Solving this system of equations yields \(w_H = 1.6\), \(w_L = 1.6\), and \(w_U = -2.2\).

16. With respect to the comments of Stiles and McCracken concerning for whom the GDP Fund would be appropriate: a. McCracken is correct and Stiles is wrong. The GDP Fund has a unit sensitivity to real GDP, meaning its returns will mirror the macroeconomic business cycle. This volatility is unsuitable for retirees looking for steady income, making Stiles wrong. However, if supply-side macro policies stimulate the economy, the fund’s GDP exposure positions it well to capture gains, making McCracken right.


Part 2: Questions Using R Codes (40%)

# Load necessary libraries
library(dplyr)
library(tidyquant)
library(lubridate)
library(timetk)
library(purrr)
library(quadprog) 
library(tidyr)
library(ggplot2)

# ==========================================
# 1. Import Data
# ==========================================
tickers <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")
start_date <- "2010-01-01"
end_date <- "2026-06-05" 

# Download daily adjusted prices
price_data <- tq_get(tickers, 
                     get = "stock.prices", 
                     from = start_date,
                     to = end_date) %>%
  dplyr::select(symbol, date, adjusted)

# ==========================================
# 2. Calculate Weekly and Monthly Returns
# ==========================================
# Weekly returns
weekly_returns <- price_data %>%
  group_by(symbol) %>%
  tq_transmute(select = adjusted, 
               mutate_fun = periodReturn, 
               period = "weekly", 
               type = "arithmetic", 
               col_rename = "weekly.returns")

# Monthly returns
monthly_returns <- price_data %>%
  group_by(symbol) %>%
  tq_transmute(select = adjusted, 
               mutate_fun = periodReturn, 
               period = "monthly", 
               type = "arithmetic", 
               col_rename = "monthly.returns")

# ==========================================
# 3. Convert Monthly Returns into Tibble Format
# ==========================================
monthly_returns_wide <- monthly_returns %>%
  pivot_wider(names_from = symbol, values_from = monthly.returns) %>%
  tk_tbl(rename_index = "date") %>%
  mutate(date = floor_date(date, "month"))

# ==========================================
# 4. Load Local Fama French 3 Factors Data
# ==========================================
ff_raw <- read.csv("F-F_Research_Data_Factors.csv", skip = 3, header = TRUE)

ff_clean <- ff_raw %>%
  rename(date = 1, Mkt_RF = `Mkt.RF`, SMB = SMB, HML = HML, RF = RF) %>%
  filter(nchar(date) == 6) %>% 
  mutate(date = ymd(paste0(date, "01")),
         Mkt_RF = as.numeric(Mkt_RF) / 100,
         SMB = as.numeric(SMB) / 100,
         HML = as.numeric(HML) / 100,
         RF = as.numeric(RF) / 100) %>%
  drop_na()

# ==========================================
# 5. Merge Monthly Return Data
# ==========================================
merged_data <- inner_join(monthly_returns_wide, ff_clean, by = "date") %>%
  drop_na()

# ==========================================
# 6. CAPM Covariance Matrix & GMV Weights 
# ==========================================
# Filter data for 60-month window: 2010/02 to 2015/01
train_data <- merged_data %>%
  filter(date >= ymd("2010-02-01") & date <= ymd("2015-01-31"))

assets <- train_data %>% dplyr::select(all_of(tickers)) %>% as.matrix()
mkt <- train_data$Mkt_RF

capm_betas <- apply(assets, 2, function(x) cov(x, mkt) / var(mkt))
capm_resid_var <- apply(assets, 2, function(x) var(lm(x ~ mkt)$residuals))
cov_mkt <- var(mkt)

cov_capm <- as.matrix(capm_betas) %*% t(as.matrix(capm_betas)) * cov_mkt + diag(capm_resid_var)

# Function to calculate GMV weights using quadprog
get_gmv_weights <- function(cov_matrix) {
  n <- ncol(cov_matrix)
  Dmat <- 2 * cov_matrix
  dvec <- rep(0, n)
  Amat <- cbind(rep(1, n), diag(n))
  bvec <- c(1, rep(0, n))
  result <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
  weights <- result$solution
  names(weights) <- colnames(cov_matrix)
  return(weights)
}

weights_capm <- get_gmv_weights(cov_capm)

# Realized portfolio return on 2015/02
ret_2015_02 <- merged_data %>% filter(date == ymd("2015-02-01")) %>% dplyr::select(all_of(tickers)) %>% as.numeric()
port_ret_capm_2015_02 <- sum(weights_capm * ret_2015_02)
print(paste("CAPM GMV Realized Return for 2015/02:", round(port_ret_capm_2015_02, 4)))
## [1] "CAPM GMV Realized Return for 2015/02: -0.0073"
# ==========================================
# 7. FF3 Covariance Matrix & GMV Weights
# ==========================================
ff_factors <- train_data %>% dplyr::select(Mkt_RF, SMB, HML) %>% as.matrix()

b_ff <- matrix(NA, nrow=8, ncol=3)
resid_var_ff <- rep(NA, 8)
for (i in 1:8) {
  model <- lm(assets[,i] ~ ff_factors)
  b_ff[i,] <- coef(model)[-1]
  resid_var_ff[i] <- var(model$residuals)
}

cov_ff3_factors <- cov(ff_factors)
cov_ff3 <- b_ff %*% cov_ff3_factors %*% t(b_ff) + diag(resid_var_ff)
colnames(cov_ff3) <- tickers

weights_ff3 <- get_gmv_weights(cov_ff3)

# Realized portfolio return on 2015/02
port_ret_ff3_2015_02 <- sum(weights_ff3 * ret_2015_02)
print(paste("FF3 GMV Realized Return for 2015/02:", round(port_ret_ff3_2015_02, 4)))
## [1] "FF3 GMV Realized Return for 2015/02: -0.0062"
# ==========================================
# 8. Backtest Performance of GMV Portfolios 
# ==========================================
start_idx <- which(merged_data$date == ymd("2015-02-01"))
end_idx <- nrow(merged_data)

port_returns_capm <- numeric(end_idx - start_idx + 1)
port_returns_ff3 <- numeric(end_idx - start_idx + 1)
dates_backtest <- merged_data$date[start_idx:end_idx]

for (i in start_idx:end_idx) {
  # Rolling window (t-60 to t-1)
  window_data <- merged_data[(i - 60):(i - 1), ]
  
  w_assets <- window_data %>% dplyr::select(all_of(tickers)) %>% as.matrix()
  w_mkt <- window_data$Mkt_RF
  w_ff_factors <- window_data %>% dplyr::select(Mkt_RF, SMB, HML) %>% as.matrix()
  
  # CAPM Weights
  w_capm_betas <- apply(w_assets, 2, function(x) cov(x, w_mkt) / var(w_mkt))
  w_capm_resid_var <- apply(w_assets, 2, function(x) var(lm(x ~ w_mkt)$residuals))
  w_cov_capm <- as.matrix(w_capm_betas) %*% t(as.matrix(w_capm_betas)) * var(w_mkt) + diag(w_capm_resid_var)
  colnames(w_cov_capm) <- tickers
  w_weights_capm <- get_gmv_weights(w_cov_capm)
  
  # FF3 Weights
  w_b_ff <- matrix(NA, nrow=8, ncol=3)
  w_resid_var_ff <- rep(NA, 8)
  for (j in 1:8) {
    mod <- lm(w_assets[,j] ~ w_ff_factors)
    w_b_ff[j,] <- coef(mod)[-1]
    w_resid_var_ff[j] <- var(mod$residuals)
  }
  w_cov_ff3 <- w_b_ff %*% cov(w_ff_factors) %*% t(w_b_ff) + diag(w_resid_var_ff)
  colnames(w_cov_ff3) <- tickers
  w_weights_ff3 <- get_gmv_weights(w_cov_ff3)
  
  # Realized Next-Month Return (t)
  current_returns <- merged_data[i, ] %>% dplyr::select(all_of(tickers)) %>% as.numeric()
  
  port_returns_capm[i - start_idx + 1] <- sum(w_weights_capm * current_returns)
  port_returns_ff3[i - start_idx + 1] <- sum(w_weights_ff3 * current_returns)
}

# Cumulative Returns
backtest_results <- tibble(
  date = dates_backtest,
  Ret_CAPM_GMV = port_returns_capm,
  Ret_FF3_GMV = port_returns_ff3
) %>%
  mutate(CumRet_CAPM = cumprod(1 + Ret_CAPM_GMV) - 1,
         CumRet_FF3 = cumprod(1 + Ret_FF3_GMV) - 1)

# Plotting Cumulative Returns
backtest_results %>%
  dplyr::select(date, CumRet_CAPM, CumRet_FF3) %>%
  pivot_longer(cols = c(CumRet_CAPM, CumRet_FF3), names_to = "Model", values_to = "Cumulative_Return") %>%
  ggplot(aes(x = date, y = Cumulative_Return, color = Model)) +
  geom_line(size = 1) +
  theme_minimal() +
  labs(title = "Cumulative Returns of GMV Portfolios (2015-2026)",
       x = "Date",
       y = "Cumulative Return",
       color = "Portfolio Model")

# Show recent cumulative returns 
tail(backtest_results %>% dplyr::select(date, CumRet_CAPM, CumRet_FF3))
## # A tibble: 6 × 3
##   date       CumRet_CAPM CumRet_FF3
##   <date>           <dbl>      <dbl>
## 1 2025-11-01        2.03       1.65
## 2 2025-12-01        2.06       1.68
## 3 2026-01-01        2.28       1.87
## 4 2026-02-01        2.48       2.05
## 5 2026-03-01        2.19       1.80
## 6 2026-04-01        2.27       1.86