Part 1: Questions from Textbook

Chapter 7

CFA 1

  1. Impact on Risk: Limiting the portfolio from 40 to 20 stocks will increase the unsystematic (firm-specific) risk of the portfolio. Because Hennessy is a “bottom-up” manager that avoids market timing, reducing the number of holdings decreases diversification, making the total portfolio variance more sensitive to the idiosyncratic shocks of individual companies.

  2. Risk Mitigation Strategy: Hennessy could minimize this risk increase by selecting the 20 stocks from uncorrelated industries or sectors. If the chosen 20 stocks exhibit low or negative correlations with one another, the portfolio can achieve a level of diversification close to that of a 40-stock portfolio.

CFA 2

Further reducing the portfolio to 10 stocks is less likely to be advantageous because diversification benefits decrease at an accelerating rate as the asset count drops below 20.

  • Maintaining 20 stocks still captures the vast majority of non-systematic risk reduction.Dropping to 10 stocks exposes the fund to extreme firm-specific risk.

  • If even one of those 10 stocks underperforms significantly, it will severely drag down the entire portfolio’s return, offsetting Hennessy’s stock-picking edge.

CFA 3

From the total fund perspective (the entire $280 million across all 6 managers), the Hennessy portfolio represents only about 10.7% of total assets (\[ \frac{\$30\text{M}}{\$30\text{M} + \$250\text{M}}\ \]).

  • Because the other five managers hold a highly diversified group of over 150 stocks, the unsystematic risk of Hennessy’s concentrated portfolio will be heavily diversified away when aggregated into the total fund.

  • Therefore, a broader view favors concentration (10 or 20 stocks) to maximize Hennessy’s alpha generation without significantly altering the risk profile of the complete pension fund.

CFA 4

Correct Answer: d. Portfolio Y

Justification: According to Markowitz efficient frontier rules, an efficient portfolio must maximize expected return for a given level of risk. Portfolio X offers an expected return of 12% with a standard deviation of 15%. Portfolio Y has a lower expected return (9%) but carries a much higher standard deviation (21%) than Portfolio X. No rational investor would choose Y over X; thus, Y cannot lie on the efficient frontier.

CFA 10

Recommendation: Portfolio of Equal Amounts of B and C

Justification:
The variance of a two-asset equal-weighted portfolio (\(w = 0.5\)) is calculated using:

\[\sigma_p^2 = (0.5)^2\sigma_1^2 + (0.5)^2\sigma_2^2 + 2(0.5)(0.5)\sigma_1\sigma_2\rho_{12}\]

Portfolio A & B

\[\sigma_{AB}^2 = 0.25(40^2) + 0.25(20^2) + 0.5(40)(20)(0.90)\]

\[= 400 + 100 + 360 = 860\]

\[\sigma_{AB} \approx 29.33\%\]

Portfolio B & C

\[\sigma_{BC}^2 = 0.25(20^2) + 0.25(40^2) + 0.5(20)(40)(0.10)\]

\[= 100 + 400 + 40 = 540\]

\[\sigma_{BC} \approx 23.24\%\]

Conclusion:
Because the correlation coefficient between B and C (\(\rho_{BC} = 0.10\)) is significantly lower than that between A and B (\(\rho_{AB} = 0.90\)), Portfolio B & C achieves superior diversification and lower overall risk.

Chapter 8

CFA 1

  • Alpha (\(\alpha\)): ABC generated a negative abnormal return (-3.20%), while XYZ produced a strong positive abnormal return (7.3%) relative to the index over the 5-year period.

  • Beta (\(\beta\)): ABC (\(\beta = 0.60\)) is less volatile than the market, whereas XYZ (\(\beta = 0.97\)) moves closely with market systematic movements.

  • \(R^2\): 35% of ABC’s variance is explained by the market (65% is firm-specific). For XYZ, only 17% is market-driven, meaning 83% of its risk is specific.

  • Implications: In a well-diversified portfolio, firm-specific risk is diversified away, leaving systematic risk (\(\beta\)) as the primary metric. The updated broker data suggests XYZ’s systematic risk is rising sharply (\(\beta\) up to 1.25–1.45), meaning analysts should expect XYZ to add more systematic risk to a portfolio going forward than historical data indicates.

CFA 2

Calculation: The coefficient of determination (\(R^2\)) represents systematic risk: \(R^2 = (\rho)^2 = (0.70)^2 = 0.49 \text{ or } 49\%\).

The specific (nonsystematic) risk is: \(1 - R^2 = 1 - 0.49 = 0.51\).

Answer: 51% of Baker Fund’s total risk is specific.

CFA 3

Using the CAPM equation: \(E(R_i) = R_f + \beta_i [E(R_m) - R_f]\).

\[9\% = 3\% + \beta_i (11\% - 3\%) \implies 6\% = \beta_i (8\%) \implies \beta_i = 0.75\]

Answer: The implied beta is 0.75

CFA 4

Correct Answer: d. Systematic risk.

CFA 5

Correct Answer: b. Only systematic risk, while standard deviation measures total risk.

Chapter 9

CFA 8

Using the CAPM benchmark return equation: \(E(R_R) = R_f + \beta_R [E(R_{S\&P}) - R_f]\).

Assuming a baseline \(R_f = 3\%\):

\[E(R_R) = 3\% + 0.5 \times (14\% - 3\%) = 8.5\%\]

Because Portfolio R’s actual return is 11%, which is higher than 8.5%, it generates positive alpha (\(\alpha = +2.5\%\)).

Correct Answer: c. Above the SML.

CFA 9

Comparing Sharpe ratios against the S&P 500 Market Index assuming \(R_f = 4\%\):

\[\text{Sharpe}_{S\&P} = \frac{14\% - 4\%}{12\%} = 0.833, \quad \text{Sharpe}_{R} = \frac{11\% - 4\%}{10\%} = 0.700\]

Since Portfolio R has a lower Sharpe ratio than the market index, it underperforms the efficient standard of the CML.

Correct Answer: b. Below the CML.

CFA 10

According to the CAPM framework, investors should NOT expect a higher return on portfolio A than on portfolio B. CAPM assumes that specific risks can be completely eliminated via diversification. Because both portfolios possess an identical beta of 1.0, their expected returns must be exactly equal.

Chapter 10

Problem 13

Using the multi-factor APT formula:

\[E(R_{High}) = R_f + \beta_{GDP}\lambda_{GDP} + \beta_{Inf}\lambda_{Inf}\]

\[E(R_{High}) = 4\% + (1.25 \times 8\%) + (1.5 \times 2\%) = 4\% + 10\% + 3\% = 17\%\]

Answer: The estimated expected return is 17%.

Problem 14

Calculate the equilibrium return for the Large Cap Fund:

\[E(R_{Large}) = R_f + (0.75 \times 8\%) + (1.25 \times 2\%) = R_f + 8.5\%\]

Kwon notes that fundamental analysis yields an expected return exactly equal to \(8.5\%\) over the risk-free rate (\(R_f + 8.5\%\)). Because the fundamental valuation matches the multi-factor model pricing perfectly, no arbitrage opportunity is available.

Problem 15

Setting up a system of linear equations to construct the target portfolio (“GDP Fund”):

  1. \(w_1 + w_2 + w_3 = 1\)

  2. \(1.25w_1 + 0.75w_2 + 1.0w_3 = 1\)

  3. \(1.5w_1 + 1.25w_2 + 2.0w_3 = 0\)

Solving this yields: \(w_1 = -1.2\), \(w_2 = 5.4\), \(w_3 = -3.2\).

Correct Answer: (b) -3.2

Problem 16

Correct Answer: a. McCracken is correct and Stiles is wrong.

Justification: A fund built with zero sensitivity to inflation and a unit exposure to real GDP growth behaves like a pure play on macroeconomic productivity. Retirees typically require explicit protection against cost-of-living index changes (inflation hedges), which this portfolio explicitly zeroes out.

Part 2: Questions Using R Codes

Q1: Import Data

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

prices_daily <- tq_get(tickers,
                       from = "2010-01-01",
                       to = "2026-06-01",
                       get = "stock.prices") %>%
  group_by(symbol) %>%
  select(date, symbol, adjusted) %>%
  spread(key = symbol, value = adjusted)

head(prices_daily)
## # A tibble: 6 × 9
##   date         EEM   EFA   GLD   IWM   IYR   QQQ   SPY   TLT
##   <date>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2010-01-04  30.4  35.1  110.  51.4  26.8  40.3  84.8  55.7
## 2 2010-01-05  30.6  35.2  110.  51.2  26.8  40.3  85.0  56.1
## 3 2010-01-06  30.6  35.3  112.  51.1  26.8  40.0  85.1  55.3
## 4 2010-01-07  30.5  35.2  111.  51.5  27.1  40.1  85.4  55.4
## 5 2010-01-08  30.7  35.5  111.  51.8  26.9  40.4  85.7  55.4
## 6 2010-01-11  30.6  35.7  113.  51.6  27.0  40.2  85.8  55.1

Q2 & Q3: Calculate Returns and Convert to Tibble

prices_xts <- tk_xts(prices_daily, date_var = date)

# Calculate periodic returns using standard simple returns
returns_weekly <- Return.calculate(prices_xts, method = "simple") %>% na.omit()
returns_monthly_m <- to.monthly(prices_xts, indexAt = "lastof", OHLC = FALSE)
returns_monthly <- Return.calculate(returns_monthly_m, method = "simple") %>% na.omit()

# Convert back to tibble format
returns_monthly_tbl <- tk_tbl(returns_monthly, rename_index = "date")
head(returns_monthly_tbl)
## # A tibble: 6 × 9
##   date            EEM      EFA      GLD     IWM     IYR     QQQ     SPY      TLT
##   <date>        <dbl>    <dbl>    <dbl>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl>
## 1 2010-02-28  0.0178   0.00267  0.0327   0.0448  0.0546  0.0460  0.0312 -0.00342
## 2 2010-03-31  0.0811   0.0639  -0.00439  0.0823  0.0975  0.0771  0.0609 -0.0206 
## 3 2010-04-30 -0.00166 -0.0280   0.0588   0.0568  0.0639  0.0224  0.0155  0.0332 
## 4 2010-05-31 -0.0939  -0.112    0.0305  -0.0754 -0.0568 -0.0739 -0.0795  0.0511 
## 5 2010-06-30 -0.0140  -0.0206   0.0236  -0.0774 -0.0467 -0.0598 -0.0517  0.0580 
## 6 2010-07-31  0.109    0.116   -0.0509   0.0673  0.0940  0.0726  0.0683 -0.00946

Q4: Download Fama-French 3 Factors Data

library(zoo)
library(xts)

# Adjust path to where you saved the file
ff3_raw <- read.csv("F-F_Research_Data_Factors.csv",
                    skip=4, stringsAsFactors=FALSE)

# Keep only rows with YYYYMM format
ff3_clean <- ff3_raw[grepl("^[0-9]{6}$", ff3_raw$X), ]
colnames(ff3_clean)[1:5] <- c("Date","Mkt_RF","SMB","HML","RF")

# Convert date
ff3_clean$Date <- as.Date(as.yearmon(ff3_clean$Date, "%Y%m"))

# Convert to decimals
ff3_clean$Mkt_RF <- as.numeric(ff3_clean$Mkt_RF)/100
ff3_clean$SMB    <- as.numeric(ff3_clean$SMB)/100
ff3_clean$HML    <- as.numeric(ff3_clean$HML)/100
ff3_clean$RF     <- as.numeric(ff3_clean$RF)/100

# Convert to xts
ff3_xts <- xts(ff3_clean[, -1], order.by = ff3_clean$Date)
head(ff3_xts)
##             Mkt_RF     SMB     HML     RF
## 1926-07-01  0.0289 -0.0255 -0.0239 0.0022
## 1926-08-01  0.0264 -0.0114  0.0381 0.0025
## 1926-09-01  0.0038 -0.0136  0.0005 0.0023
## 1926-10-01 -0.0327 -0.0014  0.0082 0.0032
## 1926-11-01  0.0254 -0.0011 -0.0061 0.0031
## 1926-12-01  0.0262 -0.0007  0.0006 0.0028

Question 5

# From Q3
prices_xts <- tk_xts(prices_daily, date_var = date)
returns_monthly_m <- to.monthly(prices_xts, indexAt = "lastof", OHLC = FALSE)
returns_monthly <- Return.calculate(returns_monthly_m, method = "simple") %>% na.omit()

# Convert to tibble
returns_monthly_tbl <- tk_tbl(returns_monthly, rename_index = "date")
head(returns_monthly_tbl)
## # A tibble: 6 × 9
##   date            EEM      EFA      GLD     IWM     IYR     QQQ     SPY      TLT
##   <date>        <dbl>    <dbl>    <dbl>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl>
## 1 2010-02-28  0.0178   0.00267  0.0327   0.0448  0.0546  0.0460  0.0312 -0.00342
## 2 2010-03-31  0.0811   0.0639  -0.00439  0.0823  0.0975  0.0771  0.0609 -0.0206 
## 3 2010-04-30 -0.00166 -0.0280   0.0588   0.0568  0.0639  0.0224  0.0155  0.0332 
## 4 2010-05-31 -0.0939  -0.112    0.0305  -0.0754 -0.0568 -0.0739 -0.0795  0.0511 
## 5 2010-06-30 -0.0140  -0.0206   0.0236  -0.0774 -0.0467 -0.0598 -0.0517  0.0580 
## 6 2010-07-31  0.109    0.116   -0.0509   0.0673  0.0940  0.0726  0.0683 -0.00946
ff3_tbl <- tk_tbl(ff3_xts, rename_index = "date")
head(ff3_tbl)
## # A tibble: 6 × 5
##   date        Mkt_RF     SMB     HML     RF
##   <date>       <dbl>   <dbl>   <dbl>  <dbl>
## 1 1926-07-01  0.0289 -0.0255 -0.0239 0.0022
## 2 1926-08-01  0.0264 -0.0114  0.0381 0.0025
## 3 1926-09-01  0.0038 -0.0136  0.0005 0.0023
## 4 1926-10-01 -0.0327 -0.0014  0.0082 0.0032
## 5 1926-11-01  0.0254 -0.0011 -0.0061 0.0031
## 6 1926-12-01  0.0262 -0.0007  0.0006 0.0028
returns_monthly_tbl <- returns_monthly_tbl %>%
  dplyr::mutate(date = zoo::as.yearmon(date))

ff3_tbl <- ff3_tbl %>%
  dplyr::mutate(date = zoo::as.yearmon(date))

merged_tbl <- dplyr::inner_join(returns_monthly_tbl, ff3_tbl, by = "date")

# Preview merged tibble
head(merged_tbl)
## # A tibble: 6 × 13
##   date           EEM      EFA      GLD     IWM     IYR     QQQ     SPY      TLT
##   <yearmon>    <dbl>    <dbl>    <dbl>   <dbl>   <dbl>   <dbl>   <dbl>    <dbl>
## 1 Feb 2010   0.0178   0.00267  0.0327   0.0448  0.0546  0.0460  0.0312 -0.00342
## 2 Mar 2010   0.0811   0.0639  -0.00439  0.0823  0.0975  0.0771  0.0609 -0.0206 
## 3 Apr 2010  -0.00166 -0.0280   0.0588   0.0568  0.0639  0.0224  0.0155  0.0332 
## 4 May 2010  -0.0939  -0.112    0.0305  -0.0754 -0.0568 -0.0739 -0.0795  0.0511 
## 5 Jun 2010  -0.0140  -0.0206   0.0236  -0.0774 -0.0467 -0.0598 -0.0517  0.0580 
## 6 Jul 2010   0.109    0.116   -0.0509   0.0673  0.0940  0.0726  0.0683 -0.00946
## # ℹ 4 more variables: Mkt_RF <dbl>, SMB <dbl>, HML <dbl>, RF <dbl>

Question 6 & 7

# Global Minimum Variance helper function
compute_gmv_weights <- function(cov_matrix) {
  inv_cov <- solve(cov_matrix)
  ones <- rep(1, ncol(cov_matrix))
  weights <- (inv_cov %*% ones) / as.numeric(t(ones) %*% inv_cov %*% ones)
  return(t(weights))
}

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

# Filter training dataset (60 months)
train_data <- merged_tbl %>% 
  filter(date >= "2010-02-01" & date <= "2015-01-01")

# Extract realized returns for February 2015
realized_feb2015 <- merged_tbl %>% 
  filter(date == "2015-02-01") %>% 
  select(all_of(asset_cols)) %>% 
  as.matrix()

# --- Question 6: CAPM Sample Covariance Strategy ---
cov_capm <- cov(train_data %>% select(all_of(asset_cols)))
weights_capm <- compute_gmv_weights(cov_capm)
ret_capm_feb15 <- sum(weights_capm * realized_feb2015)

# --- Question 7: Fama-French Factor Covariance Strategy ---
X <- as.matrix(cbind(1, train_data %>% select(Mkt_RF, SMB, HML)))
Y <- as.matrix(train_data %>% select(all_of(asset_cols)))
B_hat <- solve(t(X) %*% X) %*% t(X) %*% Y # Factor sensitivities

residuals <- Y - X %*% B_hat
D_residual <- diag(diag(cov(residuals))) # Idiosyncratic risk
F_cov <- cov(X[, 2:4]) # Factor covariance
cov_ff <- t(B_hat[2:4, ]) %*% F_cov %*% B_hat[2:4, ] + D_residual

weights_ff <- compute_gmv_weights(cov_ff)
ret_ff_feb15 <- sum(weights_ff * realized_feb2015)

# Print out your results
cat("CAPM GMV Realized Return (Feb 2015):", round(ret_capm_feb15 * 100, 4), "%\n")
## CAPM GMV Realized Return (Feb 2015): 0.8433 %
cat("FF3 GMV Realized Return (Feb 2015):", round(ret_ff_feb15 * 100, 4), "%\n")
## FF3 GMV Realized Return (Feb 2015): -0.6545 %

Question 8: Performance Backtesting Engine

# 1. Explicitly define the date sequence using your yearmon column from merged_tbl
date_sequence <- merged_tbl %>% 
  filter(date >= zoo::as.yearmon("2015-01") & date < zoo::as.yearmon("2026-05")) %>% 
  pull(date) %>% 
  unique() %>% 
  sort()
results <- tibble()

for(i in 1:length(date_sequence)) {
  current_date <- date_sequence[i]
  
  # Yearmon arithmetic: 59 months is 59/12 of a year
  window_start <- current_date - (59 / 12)
  
  # Step out a rolling 60-month slice from your merged_tbl
  window_data <- merged_tbl %>% 
    filter(date >= window_start & date <= current_date)
  
  if(nrow(window_data) == 60) {
    # Yearmon arithmetic: 1 month forward is 1/12 of a year
    next_month <- current_date + (1 / 12)
    realized_row <- merged_tbl %>% filter(date == next_month)
    
    if(nrow(realized_row) > 0) {
      R_next <- realized_row %>% select(all_of(asset_cols)) %>% as.matrix()
      
      # CAPM Weighting Loop
      Sigma_c <- cov(window_data %>% select(all_of(asset_cols)))
      w_c <- compute_gmv_weights(Sigma_c)
      r_c <- sum(w_c * R_next)
      
      # FF Weighting Loop (Using your exact cleaned column names from Q4)
      X_l <- as.matrix(cbind(1, window_data %>% select(Mkt_RF, SMB, HML)))
      Y_l <- as.matrix(window_data %>% select(all_of(asset_cols)))
      B_l <- solve(t(X_l) %*% X_l) %*% t(X_l) %*% Y_l
      res_l <- Y_l - X_l %*% B_l
      cov_ff_l <- t(B_l[2:4, ]) %*% cov(X_l[, 2:4]) %*% B_l[2:4, ] + diag(diag(cov(res_l)))
      
      w_ff <- compute_gmv_weights(cov_ff_l)
      r_ff <- sum(w_ff * R_next)
      
      # Store results (converting date back to Date class for optimal ggplot behavior)
      results <- bind_rows(results, tibble(
        date = as.Date(next_month), 
        CAPM_Return = r_c, 
        FF_Return = r_ff
      ))
    }
  }
}

# Calculate cumulative returns for plotting
portfolio_cum <- results %>%
  mutate(Cum_CAPM = cumprod(1 + CAPM_Return) - 1,
         Cum_FF = cumprod(1 + FF_Return) - 1)

# Render the comparison curves
ggplot(portfolio_cum, aes(x = date)) +
  geom_line(aes(y = Cum_CAPM, color = "CAPM GMV Model"), size = 1) +
  geom_line(aes(y = Cum_FF, color = "Fama-French GMV Model"), size = 1) +
  labs(title = "GMV Portfolio Strategy Cumulative Growth Comparison (2015 - 2026)",
       y = "Cumulative Compounded Return", x = "Timeline") +
  theme_minimal() +
  scale_color_manual(values = c("CAPM GMV Model" = "royalblue", "Fama-French GMV Model" = "darkorange"))