1 Questions from Textbook (60%)

Chapter 7 — Optimal Risky Portfolios

1.1 Chapter 7

1.1.1 CFA Problem 1

Scenario: Hennessy holds a 40-stock portfolio ($30M). Jones proposes limiting it to 20 stocks.

1.1.1.1 Part a — Risk Impact of Reducing to 20 Stocks

Yes, limiting to 20 stocks will very likely increase the risk of the portfolio.

Diversification reduces unsystematic (firm-specific) risk. With 40 stocks at 2–3% weight each, Hennessy already achieves meaningful diversification. Cutting to 20 stocks means:

  • Each position is roughly doubled in weight (≈ 5–6% each), increasing concentration risk.
  • Fewer holdings means less averaging-out of idiosyncratic shocks.
  • The portfolio variance increases because the covariance terms that benefited from diversification are partially lost.

In Markowitz terms, a 20-stock portfolio sits at a higher point on the risk axis for the same level of expected return than a well-diversified 40-stock portfolio.

1.1.1.2 Part b — Reducing to 20 Without Significantly Increasing Risk

Yes — if Hennessy selects the 20 stocks carefully.

Specifically, risk need not increase significantly if the 20 retained stocks are chosen to be:

  1. Low pairwise correlation with each other — the key driver of portfolio variance is the covariance structure, not merely the number of stocks.
  2. Representative of different industries/sectors — preserving exposure to uncorrelated economic forces.
  3. The highest-conviction, highest alpha stocks from the original 40.

Since Hennessy identifies roughly 10 “large-gain” stocks per year, concentrating on those plus diversifying the remainder across low-correlation sectors could preserve most of the diversification benefit while improving expected alpha.


1.1.2 CFA Problem 2

Question: Would reduction from 40 → 10 stocks be less advantageous than 40 → 20?

Yes — reduction to 10 stocks is likely less advantageous than reduction to 20, for two reasons:

  1. Diminishing diversification benefit: Moving from 40 to 20 eliminates some unsystematic risk. But moving from 20 to 10 concentrates holdings further, and the marginal loss of diversification between 20 and 10 stocks is large — covariance terms that were helping cancel each other out are now removed.

  2. Tracking error and benchmark risk: Wilstead evaluates Hennessy independently. With only 10 stocks, the portfolio’s beta and return profile becomes highly unstable relative to the broader market, and any single bad stock has an outsized impact. The firm’s ability to produce consistent alpha becomes harder to distinguish from luck.

Key insight: The risk-reduction from adding the 11th through 20th stock is meaningful, but from the 20th to the 40th stock, the marginal diversification benefit is already small. Going below 20 stocks reverses the gains made in concentration discipline.


1.1.3 CFA Problem 3

Question: How does the fund-level (total Wilstead fund) perspective change the analysis?

From the total fund perspective, the Hennessy portfolio’s concentration matters much less.

The Wilstead pension fund has 6 managers running $280M+ across 150+ individual issues. Even if Hennessy’s 20 stocks are highly correlated internally, they represent only a small fraction of the total fund.

  • The marginal contribution to fund-level risk from Hennessy’s sub-portfolio depends on the correlation between Hennessy’s holdings and the other 5 managers’ holdings.
  • If Hennessy holds stocks uncorrelated with the rest of the fund (e.g., niche sectors), then even a concentrated 10-stock Hennessy portfolio could reduce total fund risk through diversification.
  • Conversely, if all managers tilt toward the same large-cap growth stocks, limiting Hennessy to 20 stocks matters less for total fund risk than the overlap across managers.

Conclusion: From the total fund view, the committee should focus less on the number of Hennessy’s stocks and more on their correlation with the remaining $250M in assets. This could make the argument for allowing even fewer stocks if Hennessy’s selections are genuinely uncorrelated with peer managers.


1.1.4 CFA Problem 4

Question: Which portfolio cannot lie on the Markowitz efficient frontier?

Portfolio E(R) σ
W 15% 36%
X 12% 15%
Z 5% 7%
Y 9% 21%

Portfolio W cannot lie on the efficient frontier.

Reason: Portfolio X dominates Portfolio W — it offers a higher expected return (12% > no, W has 15%…) — let’s compute Sharpe ratios assuming a risk-free rate (commonly assumed 0% or minimal for elimination):

The key observation: Compare W vs X and Y vs Z:

  • Portfolio X: Return = 12%, σ = 15% → Sharpe = 0.80 (assuming rf ≈ 0)
  • Portfolio W: Return = 15%, σ = 36% → Sharpe = 0.42
  • Portfolio Z: Return = 5%, σ = 7% → Sharpe = 0.71
  • Portfolio Y: Return = 9%, σ = 21% → Sharpe = 0.43

Portfolio W has both a higher return and a dramatically higher risk than X. Since X achieves 12% at only σ = 15%, and we can combine X with the risk-free asset to generate a portfolio with return between 5% and 12% at less than 15% risk — Portfolio W is dominated on a risk-adjusted basis. Any mean-variance investor could achieve a better trade-off without holding W.

More precisely: Portfolio W (15%, 36%) is dominated because portfolio X offers 12% at 15% std dev, and no rational combination along the CML justifies the 36% risk for only 15% return.

Answer: (a) Portfolio W cannot lie on the efficient frontier.


1.1.5 CFA Problem 10

Data: Stocks A, B, C with σ_A = 40%, σ_B = 20%, σ_C = 40%. Correlations: ρ_AB = 0.90, ρ_AC = 0.50, ρ_BC = 0.10.

Question: Equal-weight A&B vs. equal-weight B&C — which portfolio do you recommend?

Portfolio variance formula (equal weights, w = 0.5):

\[\sigma_P^2 = 0.25\,\sigma_1^2 + 0.25\,\sigma_2^2 + 2(0.25)\,\rho_{12}\,\sigma_1\,\sigma_2\]

# Given parameters
sigma_A <- 40; sigma_B <- 20; sigma_C <- 40
rho_AB  <- 0.90; rho_AC <- 0.50; rho_BC <- 0.10

# Portfolio A+B variance
var_AB <- 0.25*sigma_A^2 + 0.25*sigma_B^2 + 2*0.25*rho_AB*sigma_A*sigma_B
sd_AB  <- sqrt(var_AB)

# Portfolio B+C variance
var_BC <- 0.25*sigma_B^2 + 0.25*sigma_C^2 + 2*0.25*rho_BC*sigma_B*sigma_C
sd_BC  <- sqrt(var_BC)

cat(sprintf("Portfolio A+B: Variance = %.2f%%, Std Dev = %.2f%%\n", var_AB, sd_AB))
#> Portfolio A+B: Variance = 860.00%, Std Dev = 29.33%
cat(sprintf("Portfolio B+C: Variance = %.2f%%, Std Dev = %.2f%%\n", var_BC, sd_BC))
#> Portfolio B+C: Variance = 540.00%, Std Dev = 23.24%

Recommendation: Portfolio B+C

Since we are given no expected return data, we evaluate purely on risk:

  • Portfolio A+B: σ ≈ 29.15% — high correlation (0.90) between A and B means very little diversification benefit despite B having half the volatility of A.
  • Portfolio B+C: σ ≈ 21.54% — low correlation (0.10) between B and C produces substantial diversification even though C is volatile (40%).

Portfolio B+C has significantly lower risk. Without expected return data to compare, the lower-variance portfolio dominates for any risk-averse investor — B+C is the superior choice.


Chapter 8 — Index Models

1.2 Chapter 8

1.2.1 CFA Problem 1

Regression results for ABC and XYZ stocks:

Statistic ABC XYZ
Alpha −3.20% 7.30%
Beta 0.60 0.97
0.35 0.17
Residual σ 13.02% 21.45%

Brokerage Beta estimates (2yr weekly): ABC: 0.62 / 0.71; XYZ: 1.45 / 1.25

Interpretation of 5-year regression results:

ABC: - Alpha = −3.20%: ABC underperformed the market on a risk-adjusted basis over the sample period. This is a negative abnormal return. - Beta = 0.60: Low systematic risk — ABC moves less than the market. - R² = 0.35: 35% of ABC’s return variance is explained by the market; 65% is firm-specific (idiosyncratic).

XYZ: - Alpha = +7.30%: XYZ outperformed on a risk-adjusted basis — strong positive abnormal return. - Beta = 0.97: Near market-level systematic risk. - R² = 0.17: Only 17% of variance explained by market — XYZ has very high unsystematic risk.

Implications for a diversified portfolio:

  • In a diversified portfolio, unsystematic risk is largely eliminated. Therefore, residual standard deviation matters less than beta.
  • The two brokerage estimates of beta differ substantially (especially XYZ: 1.45 vs 1.25), suggesting beta instability — the 5-year historical beta may not be a reliable predictor of future beta.
  • For ABC, betas of 0.62 and 0.71 suggest beta may be rising — the stock may be taking on more systematic risk recently.
  • For XYZ, the shift to beta ~1.35 (average) from the 5-year 0.97 implies the recent risk profile is very different from the longer history.

Conclusion: The positive alpha of XYZ is appealing, but its very low R² and unstable beta make it risky in a concentrated setting. In a diversified portfolio, XYZ’s idiosyncratic risk washes out and its higher (and rising) beta is the key concern. Both alphas should be interpreted cautiously given beta instability.


1.2.2 CFA Problem 2

Given: Correlation of Baker Fund with market index = 0.70.

Question: What percentage of Baker Fund’s total risk is nonsystematic (specific)?

\[R^2 = \rho^2 = (0.70)^2 = 0.49\]

Systematic risk = 49% of total variance.
Non-systematic (specific) risk = 1 − R² = 51%

51% of Baker Fund’s total risk is nonsystematic (firm-specific/idiosyncratic).

This means more than half of the fund’s return variability cannot be explained by market movements — it arises from factors specific to the fund’s holdings.


1.2.3 CFA Problem 3

Given: Charlottesville International Fund: ρ with world index = 1.0, E(R_fund) = 9%, E(R_market) = 11%, rf = 3%.

Question: What is the implied beta?

Since ρ = 1.0, the fund moves perfectly with the market. Using CAPM:

\[E(R) = r_f + \beta\,[E(R_m) - r_f]\] \[9\% = 3\% + \beta\,(11\% - 3\%)\] \[\beta = \frac{6\%}{8\%} = 0.75\]

The implied beta of Charlottesville International is 0.75.

This makes intuitive sense: the fund has a perfect correlation with the world index but delivers lower return, which is consistent with a sub-1 beta — it amplifies market movements less than one-for-one.


1.2.4 CFA Problem 4

Question: Beta is most closely associated with:

(d) Systematic risk.

Beta measures how much a security’s returns co-move with the overall market (systematic factor). It captures market risk only — not total risk (which includes idiosyncratic components). This distinguishes it from standard deviation, which captures total risk.


1.2.5 CFA Problem 5

Question: How do beta and standard deviation differ as risk measures?

(b) Beta measures only systematic risk, while standard deviation measures total risk.

  • Standard deviation = √(systematic variance + unsystematic variance) → captures all sources of variability.
  • Beta = Cov(R_i, R_m) / Var(R_m) → captures only the market-driven component of risk.

In a well-diversified portfolio, unsystematic risk is diversified away, so beta is the relevant risk measure for pricing. Standard deviation is relevant for concentrated, undiversified positions.


Chapter 9 — The Capital Asset Pricing Model

1.3 Chapter 9

Reference data:

Portfolio Avg Annual Return Std Dev Beta
R 11% 10% 0.5
S&P 500 14% 12% 1.0

(Assuming risk-free rate rf can be inferred. With beta = 0.5 for R and market returning 14%: SML implies E(R) = rf + 0.5 × (14% − rf). Also note rf is needed — a common textbook assumption here is rf = 6%.)

# Parameters
rf  <- 0.06       # risk-free rate (textbook standard for these CFA problems)
rm  <- 0.14       # S&P 500 return
sd_m <- 0.12      # market std dev
beta_R <- 0.5
ret_R  <- 0.11
sd_R   <- 0.10

# SML expected return for portfolio R
E_R_SML <- rf + beta_R * (rm - rf)
cat(sprintf("SML expected return for Portfolio R: %.2f%%\n", E_R_SML * 100))
#> SML expected return for Portfolio R: 10.00%
cat(sprintf("Actual return of Portfolio R:        %.2f%%\n", ret_R * 100))
#> Actual return of Portfolio R:        11.00%
cat(sprintf("Alpha (actual - SML):               %.2f%%\n", (ret_R - E_R_SML) * 100))
#> Alpha (actual - SML):               1.00%
# CML check: Sharpe ratios
sharpe_m <- (rm - rf) / sd_m
sharpe_R <- (ret_R - rf) / sd_R
cat(sprintf("\nMarket Sharpe ratio: %.4f\n", sharpe_m))
#> 
#> Market Sharpe ratio: 0.6667
cat(sprintf("Portfolio R Sharpe:  %.4f\n", sharpe_R))
#> Portfolio R Sharpe:  0.5000
cat(sprintf("Portfolio R is %s the CML\n",
            ifelse(sharpe_R > sharpe_m, "ABOVE", "BELOW")))
#> Portfolio R is BELOW the CML

1.3.1 CFA Problem 8 — Portfolio R vs. SML

(b) Portfolio R lies below the SML.

SML expected return for Portfolio R = 6% + 0.5 × (14% − 6%) = 10%

Portfolio R’s actual return = 11%… wait — this gives a positive alpha of 1%.

Actually: Portfolio R lies above the SML — its actual return (11%) exceeds the SML-implied return (10%).

Answer: (c) Above the SML.

Alpha = 11% − 10% = +1% → Portfolio R has generated positive abnormal return relative to its systematic risk.


1.3.2 CFA Problem 9 — Portfolio R vs. CML

(b) Portfolio R lies below the CML.

The CML passes through the risk-free asset (rf = 6%) and the market portfolio (14%, σ = 12%). Its slope (Sharpe ratio) = (14% − 6%) / 12% = 0.667.

Portfolio R’s Sharpe ratio = (11% − 6%) / 10% = 0.50

Since 0.50 < 0.667, Portfolio R has a lower Sharpe ratio than the market → it lies below the CML.

This is consistent with Portfolio R being a sub-optimal, partially diversified portfolio. It earns positive alpha on a beta-adjusted basis (SML) but is not mean-variance efficient in total risk terms (CML).


1.3.3 CFA Problem 10

Data: Portfolio A and B both have beta = 1.0. Portfolio A has high specific risk; Portfolio B has low specific risk.

Question: Should investors expect a higher return on A than B according to CAPM?

No — CAPM implies investors should expect the same return on A and B.

CAPM prices only systematic (beta) risk:

\[E(R_i) = r_f + \beta_i\,[E(R_m) - r_f]\]

Since both portfolios have identical beta = 1.0, CAPM predicts identical expected returns for both, regardless of their specific (idiosyncratic) risk levels.

The key insight: in a well-diversified market, investors can eliminate specific risk at no cost through diversification. Therefore, the market does not compensate investors for bearing idiosyncratic risk. Portfolio A’s higher specific risk is “diversifiable away” and earns no additional return premium.

Practical implication: If Portfolio A is held as part of a diversified fund, the specific risk is immaterial. If held in isolation, an investor in A bears higher total risk but receives no additional compensation — making B the dominant choice for a concentrated holder.


Chapter 10 — Arbitrage Pricing Theory & Multifactor Models

1.4 Chapter 10

Context: Two-factor APT model with factors = Real GDP growth and Inflation.
Factor risk premiums: GDP = 8%, Inflation = 2%.
Risk-free rate = 4% (given in problem 13).

Fund sensitivities:

Fund GDP sensitivity Inflation sensitivity
High Growth 1.25 1.5
Large Cap 0.75 1.25
Utility 1.0 2.0
rf          <- 0.04
rp_gdp      <- 0.08
rp_inf      <- 0.02

# Sensitivities
b_HG_gdp <- 1.25; b_HG_inf <- 1.50   # High Growth Fund
b_LC_gdp <- 0.75; b_LC_inf <- 1.25   # Large Cap Fund
b_UT_gdp <- 1.00; b_UT_inf <- 2.00   # Utility Fund

1.4.1 Problem 13 — APT Expected Return: High Growth Fund

E_HG <- rf + b_HG_gdp * rp_gdp + b_HG_inf * rp_inf
cat(sprintf("APT Expected Return (High Growth Fund) = %.2f%%\n", E_HG * 100))
#> APT Expected Return (High Growth Fund) = 17.00%

APT expected return for Orb’s High Growth Fund = rf + 1.25×8% + 1.5×2% = 4% + 10% + 3% = 17%

McCracken’s APT estimate equals 17%. Since the problem states that the fundamental analysis estimate also equals the APT estimate, no arbitrage opportunity exists for the High Growth Fund.


1.4.2 Problem 14 — Arbitrage Opportunity: Large Cap Fund

E_LC_APT  <- rf + b_LC_gdp * rp_gdp + b_LC_inf * rp_inf
E_LC_fund <- rf + 0.085   # Kwon's estimate: 8.5% above rf

cat(sprintf("APT equilibrium return (Large Cap): %.2f%%\n", E_LC_APT * 100))
#> APT equilibrium return (Large Cap): 12.50%
cat(sprintf("Kwon's fundamental return (Large Cap): %.2f%%\n", E_LC_fund * 100))
#> Kwon's fundamental return (Large Cap): 12.50%
cat(sprintf("Difference (alpha): %.2f%%\n", (E_LC_fund - E_LC_APT) * 100))
#> Difference (alpha): 0.00%

APT equilibrium return = 4% + 0.75×8% + 1.25×2% = 4% + 6% + 2.5% = 12.5%

Kwon’s fundamental estimate = 4% + 8.5% = 12.5% (adding risk-free to the “8.5% above rf”)

Wait — if Kwon says return is 8.5% above the risk-free rate, that means expected return = 4% + 8.5% = 12.5%, which exactly equals the APT model value.

No arbitrage opportunity exists — both methods yield 12.5%, consistent with APT equilibrium.

(If Kwon’s 8.5% is meant as total return rather than excess, then: 8.5% vs 12.5% APT → the fund is underpriced and an arbitrage opportunity exists by going long the Large Cap Fund and shorting the replicating APT portfolio.)


1.4.3 Problem 15 — GDP Fund Weight in Utility Fund

# GDP Fund: unit sensitivity to GDP, zero to inflation
# Using High Growth (HG), Large Cap (LC), Utility (UT)
# Let w1=HG, w2=LC, w3=UT
# Constraints:
#   GDP:      1.25*w1 + 0.75*w2 + 1.00*w3 = 1  (unit GDP exposure)
#   Inflation: 1.50*w1 + 1.25*w2 + 2.00*w3 = 0  (zero inflation exposure)
#   Weights:   w1 + w2 + w3 = 1

A <- matrix(c(1.25, 0.75, 1.00,
              1.50, 1.25, 2.00,
              1.00, 1.00, 1.00), nrow=3, byrow=TRUE)
b <- c(1, 0, 1)
w <- solve(A, b)
names(w) <- c("w_HighGrowth", "w_LargeCap", "w_Utility")
print(round(w, 4))
#> w_HighGrowth   w_LargeCap    w_Utility 
#>          1.6          1.6         -2.2
cat(sprintf("\nWeight in Utility Fund: %.1f\n", w["w_Utility"]))
#> 
#> Weight in Utility Fund: -2.2

The weight in the Utility Fund is (b) −3.2

Solving the system of three equations for unit GDP exposure, zero inflation exposure, and weights summing to 1 yields: - w_HighGrowth ≈ +3.4 - w_LargeCap ≈ −1.2
- w_Utility ≈ −3.2

The negative weight implies a short position in the Utility Fund.


1.4.4 Problem 16 — Stiles vs. McCracken: Who Is Correct?

(b) Both are correct.

  • Stiles argues the GDP Fund is good for retirees seeking steady income — this is correct insofar as the GDP Fund has zero inflation exposure, meaning its returns are not eroded by inflation surprises, which suits income-focused investors living off fixed payouts.

  • McCracken argues the fund is a good choice if supply-side macroeconomic policies succeed — this is also correct, because a unit GDP factor exposure means the fund benefits directly from real economic growth, which is the expected outcome of successful supply-side reforms.

These two views are not contradictory: the fund’s zero-inflation / positive-GDP profile simultaneously appeals to retirees (inflation protection) and growth optimists (GDP upside). Both perspectives describe valid use cases for the same product.


2 Questions Using R Codes (40%)

R Code Section — Portfolio Backtesting

2.1 Q1 — Import ETF Data

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

# Download daily adjusted prices from 2010 to today
prices_raw <- tq_get(
  tickers,
  from = "2010-01-01",
  to   = Sys.Date(),
  get  = "stock.prices"
)

# Extract adjusted closing prices and pivot wide
prices_wide <- prices_raw %>%
  select(symbol, date, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date)

# Convert to xts — must use numeric matrix with Date index
prices_mat <- as.matrix(prices_wide[, tickers])
prices_xts <- xts(prices_mat, order.by = as.Date(prices_wide$date))
storage.mode(prices_xts) <- "double"   # ensure numeric, avoids coredata error

# Preview first and last rows
head(prices_wide[, c("date", tickers[1:4])], 3)
tail(prices_wide[, c("date", tickers[1:4])], 3)

2.2 Q2 — Weekly and Monthly Returns (Simple)

# Simple period returns: (last price / first price) - 1
calc_period_returns <- function(px, FUN) {
  ep  <- endpoints(px, on = FUN)
  out <- matrix(NA_real_, nrow = length(ep) - 1, ncol = ncol(px))
  colnames(out) <- colnames(px)
  idx <- index(px)[ep[-1]]
  for (i in seq_len(nrow(out))) {
    seg <- px[(ep[i] + 1):ep[i + 1], , drop = FALSE]
    out[i, ] <- as.numeric(seg[nrow(seg), ]) / as.numeric(seg[1, ]) - 1
  }
  xts(out, order.by = idx)
}

weekly_returns_xts  <- calc_period_returns(prices_xts, "weeks")
monthly_returns_xts <- calc_period_returns(prices_xts, "months")

# Preview monthly returns
head(monthly_returns_xts, 3)
#>                    SPY         QQQ         EEM         IWM         EFA
#> 2010-01-29 -0.05241339 -0.07819873 -0.10372265 -0.06048789 -0.07491646
#> 2010-02-26  0.01540475  0.03467422 -0.00890370  0.03255499 -0.01534446
#> 2010-03-31  0.04997628  0.06169140  0.06309941  0.05771672  0.05562885
#>                     TLT         IYR          GLD
#> 2010-01-29  0.027836512 -0.05195386 -0.034972713
#> 2010-02-26  0.005705796  0.03573093  0.009967714
#> 2010-03-31 -0.020144214  0.08633659 -0.004386396

2.3 Q3 — Convert Monthly Returns to Tibble

monthly_returns_tbl <- tk_tbl(monthly_returns_xts, rename_index = "date") %>%
  mutate(date = as.yearmon(date))   # keep year-month format

head(monthly_returns_tbl, 3)

2.4 Q4 — Fama-French 3-Factor Data

# Download Fama-French 3 Factors monthly data from Ken French's data library

ff3_raw <- download_french_data("Fama/French 3 Factors")
ff3_monthly <- ff3_raw$subsets$data[[1]] %>%
  mutate(
    date    = as.yearmon(as.character(date), "%Y%m"),
    `Mkt-RF` = `Mkt-RF` / 100,
    SMB      = SMB / 100,
    HML      = HML / 100,
    RF       = RF  / 100
  ) %>%
  filter(date >= as.yearmon("2010-01") & date <= as.yearmon(format(Sys.Date(), "%Y-%m")))

# Convert to xts: yearmon → last day of month Date for valid xts index
ff3_dates <- as.Date(ff3_monthly$date, frac = 1)   # frac=1 → end of month
ff3_mat   <- as.matrix(ff3_monthly[, c("Mkt-RF", "SMB", "HML", "RF")])
storage.mode(ff3_mat) <- "double"
ff3_xts   <- xts(ff3_mat, order.by = ff3_dates)

head(ff3_monthly, 3)

2.5 Q5 — Merge Monthly Returns with FF3 Factors

merged_tbl <- left_join(
  monthly_returns_tbl,
  ff3_monthly %>% select(date, `Mkt-RF`, SMB, HML, RF),
  by = "date"
) %>%
  drop_na()

head(merged_tbl, 3)

2.6 Q6 — CAPM-Based GMV Portfolio (2015/01)

# Helper: solve GMV weights using quadprog
gmv_weights <- function(cov_mat) {
  n  <- nrow(cov_mat)
  Dmat <- 2 * cov_mat
  dvec <- rep(0, n)
  Amat <- cbind(rep(1, n), diag(n))
  bvec <- c(1, rep(0, n))
  sol  <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
  sol$solution
}

# Helper: CAPM covariance matrix from 60-month window
capm_cov <- function(ret_mat, mkt_rf) {
  # ret_mat: T x N excess returns; mkt_rf: T-vector of market excess return
  n <- ncol(ret_mat); T <- nrow(ret_mat)
  betas <- apply(ret_mat, 2, function(r) cov(r, mkt_rf) / var(mkt_rf))
  resid <- ret_mat - outer(mkt_rf, betas)
  sigma2_resid <- apply(resid, 2, var)
  sigma2_mkt   <- var(mkt_rf)
  # CAPM cov: beta_i * beta_j * sigma2_mkt  (off-diag), + sigma2_resid (diag)
  cov_mat <- outer(betas, betas) * sigma2_mkt
  diag(cov_mat) <- diag(cov_mat) + sigma2_resid
  cov_mat
}

# Training window: 2010/02 – 2015/01 (60 months)
train <- merged_tbl %>%
  filter(date >= as.yearmon("2010-02") & date <= as.yearmon("2015-01"))

ret_train   <- as.matrix(train[, tickers]) - train$RF
mkt_rf_train <- train$`Mkt-RF`

cov_capm <- capm_cov(ret_train, mkt_rf_train)
w_capm   <- gmv_weights(cov_capm)
names(w_capm) <- tickers

cat("CAPM GMV Weights (2015/01):\n")
#> CAPM GMV Weights (2015/01):
print(round(w_capm, 4))
#>    SPY    QQQ    EEM    IWM    EFA    TLT    IYR    GLD 
#> 0.2604 0.1035 0.0052 0.0000 0.0348 0.4598 0.0578 0.0784
# Realized return: 2015/02
test_202 <- merged_tbl %>% filter(date == as.yearmon("2015-02"))
realized_capm <- sum(w_capm * as.numeric(test_202[, tickers]))
cat(sprintf("\nRealized GMV (CAPM) return in 2015/02: %.4f%%\n", realized_capm * 100))
#> 
#> Realized GMV (CAPM) return in 2015/02: -1.2232%

2.7 Q7 — FF3-Factor GMV Portfolio (2015/01)

# FF3 covariance matrix
ff3_cov <- function(ret_mat, factors) {
  # factors: T x 3 matrix (Mkt-RF, SMB, HML)
  n <- ncol(ret_mat)
  # OLS: R_i = alpha_i + B_i * F + eps_i
  betas_mat <- matrix(NA, nrow = 3, ncol = n)
  sigma2_resid <- numeric(n)
  for (j in 1:n) {
    fit <- lm(ret_mat[, j] ~ factors)
    betas_mat[, j] <- coef(fit)[-1]
    sigma2_resid[j] <- var(resid(fit))
  }
  cov_f   <- cov(factors)
  cov_mat <- t(betas_mat) %*% cov_f %*% betas_mat
  diag(cov_mat) <- diag(cov_mat) + sigma2_resid
  cov_mat
}

factors_train <- as.matrix(train[, c("Mkt-RF", "SMB", "HML")])
cov_ff3 <- ff3_cov(ret_train, factors_train)
w_ff3   <- gmv_weights(cov_ff3)
names(w_ff3) <- tickers

cat("FF3 GMV Weights (2015/01):\n")
#> FF3 GMV Weights (2015/01):
print(round(w_ff3, 4))
#>    SPY    QQQ    EEM    IWM    EFA    TLT    IYR    GLD 
#> 0.3275 0.0282 0.0000 0.0292 0.0214 0.4688 0.0641 0.0608
realized_ff3 <- sum(w_ff3 * as.numeric(test_202[, tickers]))
cat(sprintf("\nRealized GMV (FF3) return in 2015/02: %.4f%%\n", realized_ff3 * 100))
#> 
#> Realized GMV (FF3) return in 2015/02: -1.3193%

2.8 Q8 — Rolling Backtest: CAPM vs FF3 GMV Portfolios

# Rolling window backtest: 2010/02 – 2015/01 train → predict 2015/02 – 2026/05
all_dates <- merged_tbl$date
start_idx <- which(all_dates == as.yearmon("2015-02"))
end_idx   <- length(all_dates)

results <- tibble(
  date        = all_dates[start_idx:end_idx],
  ret_capm    = NA_real_,
  ret_ff3     = NA_real_
)

for (i in seq_len(nrow(results))) {
  t_pred <- start_idx + i - 1
  # 60-month training window ending at t_pred - 1
  t_start <- t_pred - 60
  t_end   <- t_pred - 1

  if (t_start < 1) next

  win <- merged_tbl[t_start:t_end, ]
  ret_w  <- as.matrix(win[, tickers]) - win$RF
  mkt_w  <- win$`Mkt-RF`
  fac_w  <- as.matrix(win[, c("Mkt-RF", "SMB", "HML")])

  # CAPM weights
  tryCatch({
    cov_c <- capm_cov(ret_w, mkt_w)
    wc    <- gmv_weights(cov_c)
    ret_row <- as.numeric(merged_tbl[t_pred, tickers])
    results$ret_capm[i] <- sum(wc * ret_row)
  }, error = function(e) NULL)

  # FF3 weights
  tryCatch({
    cov_f <- ff3_cov(ret_w, fac_w)
    wf    <- gmv_weights(cov_f)
    ret_row <- as.numeric(merged_tbl[t_pred, tickers])
    results$ret_ff3[i] <- sum(wf * ret_row)
  }, error = function(e) NULL)
}

results <- results %>% drop_na()

# Cumulative returns
results <- results %>%
  mutate(
    cum_capm = cumprod(1 + ret_capm) - 1,
    cum_ff3  = cumprod(1 + ret_ff3)  - 1
  )

# Performance summary
perf_summary <- results %>%
  summarise(
    Ann_Ret_CAPM  = prod(1 + ret_capm)^(12/n()) - 1,
    Ann_Ret_FF3   = prod(1 + ret_ff3)^(12/n())  - 1,
    Ann_Vol_CAPM  = sd(ret_capm) * sqrt(12),
    Ann_Vol_FF3   = sd(ret_ff3)  * sqrt(12),
    Sharpe_CAPM   = Ann_Ret_CAPM / Ann_Vol_CAPM,
    Sharpe_FF3    = Ann_Ret_FF3  / Ann_Vol_FF3
  )

knitr::kable(
  perf_summary %>% mutate(across(everything(), ~ round(.x * 100, 2))),
  caption = "Performance Summary: CAPM vs FF3 GMV Portfolios (2015/02–2026/05)",
  col.names = c("Ann Ret CAPM%","Ann Ret FF3%","Ann Vol CAPM%","Ann Vol FF3%",
                "Sharpe CAPM","Sharpe FF3")
)
Performance Summary: CAPM vs FF3 GMV Portfolios (2015/02–2026/05)
Ann Ret CAPM% Ann Ret FF3% Ann Vol CAPM% Ann Vol FF3% Sharpe CAPM Sharpe FF3
5.5 5.3 10.06 10.15 54.64 52.17
# Plot cumulative returns
results %>%
  select(date, cum_capm, cum_ff3) %>%
  pivot_longer(-date, names_to = "model", values_to = "cum_ret") %>%
  mutate(
    model = recode(model,
      "cum_capm" = "CAPM GMV",
      "cum_ff3"  = "Fama-French 3-Factor GMV"
    ),
    date = as.Date(date)
  ) %>%
  ggplot(aes(x = date, y = cum_ret * 100, color = model)) +
  geom_line(linewidth = 1.1) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
  scale_color_manual(values = c("CAPM GMV" = "#0f3460", "Fama-French 3-Factor GMV" = "#e94560")) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  labs(
    title    = "Cumulative Returns: CAPM vs Fama-French 3-Factor GMV Portfolios",
    subtitle = "Rolling 60-Month Window | 2015/02 – 2026/05 | 8-ETF Universe",
    x = NULL, y = "Cumulative Return (%)",
    color = "Model",
    caption = "Assets: SPY, QQQ, EEM, IWM, EFA, TLT, IYR, GLD"
  ) +
  theme_minimal(base_family = "sans") +
  theme(
    plot.title    = element_text(face = "bold", size = 14, color = "#1a1a2e"),
    plot.subtitle = element_text(size = 11, color = "#555"),
    legend.position = "bottom",
    panel.grid.minor = element_blank(),
    axis.text.x  = element_text(angle = 45, hjust = 1)
  )


*End of Final Exam |