1 Questions from Textbook (60%)


1.1 Chapter 7

1.1.1 CFA Problem 1

a. Will the limitation to 20 stocks likely increase or decrease the risk of the portfolio? Explain.

The limitation to 20 stocks will increase portfolio risk. Reducing the number of holdings from 40 to 20 reduces diversification. With fewer stocks, the portfolio retains more unsystematic (firm-specific) risk that could otherwise be diversified away. The variance reduction from diversification diminishes as the number of stocks increases, but moving from 40 down to 20 still meaningfully increases exposure to idiosyncratic risk.

b. Is there any way Hennessy could reduce the number of issues from 40 to 20 without significantly affecting risk?

Yes. Hennessy could select the 20 stocks such that their pairwise correlations are as low as possible. If Hennessy concentrates the portfolio in the 10 stocks per year that register particularly large gains — and those stocks are drawn from different industries with low inter-correlations — the remaining 20 stocks could retain risk properties close to the 40-stock portfolio. Alternatively, Hennessy could replace the dropped stocks with index funds or ETFs covering those sectors so that systematic exposure is maintained while reducing the number of individual lines.


1.1.2 CFA Problem 2

One committee member suggested reducing holdings to 10. If reducing from 40 to 20 is expected to be advantageous (higher concentration in Hennessy’s best picks), why might reducing further to 10 be less advantageous?

Answer: The benefit of concentration comes from focusing on Hennessy’s highest-conviction picks. Hennessy identifies approximately 10 outstanding stocks per year. Concentrating to 20 still allows double-weighting each of those 10 winners while retaining some diversification buffer. However, reducing to only 10 stocks eliminates that diversification buffer entirely. Any one stock experiencing an unexpected negative event would have a catastrophic impact on the portfolio (each position is ~10% of the fund). The incremental gain in concentration is outweighed by the much larger jump in unsystematic risk. In other words, at 10 stocks, idiosyncratic variance dominates, and even Hennessy’s skill cannot reliably overcome that noise.


1.1.3 CFA Problem 3

Another committee member suggests evaluating Hennessy’s portfolio in the context of the total fund (not independently). How does this broader view affect the decision to limit holdings to 10 or 20 issues?

Answer: When viewed as one component of a larger multi-manager fund ($250 million across six other managers holding 150+ stocks), the Hennessy portfolio is already highly diversified at the total-fund level. From the perspective of the total fund:

  • The other five managers already provide extensive diversification.
  • Hennessy’s portfolio is a relatively small sub-portfolio ($30M out of ~$280M total).
  • The marginal contribution of Hennessy’s individual stock diversification to total-fund risk is small.
  • Therefore, reducing Hennessy to even 10 stocks would have a negligible effect on total fund risk because the other portfolios absorb most of the idiosyncratic risk.

This broader view makes a stronger case for allowing Hennessy to concentrate further (even to 10 stocks), since the skill-based alpha generation is the objective, and the fund-level diversification is already achieved by the other managers.


1.1.4 CFA Problem 4

Which one of the following portfolios cannot lie on the efficient frontier as described by Markowitz?

Portfolio Expected Return (%) Standard Deviation (%)
W 15 36
X 12 15
Z 5 7
Y 9 21

Answer: Portfolio W cannot lie on the efficient frontier.

On the Markowitz efficient frontier, no portfolio with a higher standard deviation should offer a lower expected return relative to another feasible portfolio. Comparing W and X: Portfolio X achieves a 12% return with only 15% standard deviation, while Portfolio W offers 15% return with 36% standard deviation. More importantly, for W to be efficient, there should be no other portfolio with the same or higher return and lower risk. Given that X already achieves 12% at σ=15%, we can construct a portfolio combining X and a risk-free asset or other assets to dominate W.

The key: Portfolio W (15%, 36%) is dominated in risk-adjusted terms. The reward-to-variability ratio of W = 15/36 = 0.417, while X = 12/15 = 0.800. Any rational investor would prefer X or a leveraged position in X over W. Thus W cannot lie on the efficient frontier.

(Answer: a. W)


1.1.5 CFA Problem 10

Statistics for stocks A, B, and C:

Stock Std Dev (%) Corr with A Corr with B Corr with C
A 40 1.00 0.90 0.50
B 20 0.90 1.00 0.10
C 40 0.50 0.10 1.00

Which portfolio would you recommend: equal amounts of A & B, or equal amounts of B & C?

Calculate the variance of each equal-weight (50/50) portfolio:

Portfolio A+B variance:

\[\sigma^2_{AB} = (0.5)^2(40)^2 + (0.5)^2(20)^2 + 2(0.5)(0.5)(0.90)(40)(20)\] \[= 0.25(1600) + 0.25(400) + 2(0.25)(0.90)(800)\] \[= 400 + 100 + 360 = 860\] \[\sigma_{AB} = \sqrt{860} \approx 29.3\%\]

Portfolio B+C variance:

\[\sigma^2_{BC} = (0.5)^2(20)^2 + (0.5)^2(40)^2 + 2(0.5)(0.5)(0.10)(20)(40)\] \[= 0.25(400) + 0.25(1600) + 2(0.25)(0.10)(800)\] \[= 100 + 400 + 40 = 540\] \[\sigma_{BC} = \sqrt{540} \approx 23.2\%\]

Recommendation: Portfolio B+C.

Since expected returns are not given, we can only compare on risk. Portfolio B+C has significantly lower variance (540 vs 860) due to the very low correlation between B and C (0.10 vs 0.90 for A and B). The diversification benefit from combining B and C is far superior. Under mean-variance analysis with no return information, B+C dominates A+B on risk grounds and should be preferred.


1.2 Chapter 8

1.2.1 CFA Problem 1

Regression results for ABC and XYZ stocks:

Statistic ABC XYZ
Alpha −3.20% 7.3%
Beta 0.60 0.97
0.35 0.17
Residual std deviation 13.02% 21.45%

Interpretation and implications:

ABC: Alpha = −3.20% indicates that over the sample period, ABC underperformed its CAPM-predicted return by 3.2% per year. Beta = 0.60 means ABC has below-market systematic risk. R² = 0.35 means 35% of ABC’s return variance is explained by market movements; the remaining 65% is firm-specific (unsystematic) risk. The high residual standard deviation (13.02%) confirms significant idiosyncratic risk.

XYZ: Alpha = +7.3% suggests XYZ outperformed its CAPM benchmark by 7.3% annually over the sample period. Beta ≈ 1.0 means near-market-level systematic risk. However, R² = 0.17 is very low — only 17% of XYZ’s variance is explained by the market. The residual standard deviation of 21.45% is very large, indicating XYZ has enormous firm-specific risk.

Future implications for a diversified portfolio: When both stocks are included in a well-diversified common stock portfolio, their unsystematic risk is diversified away. Only beta (systematic risk) is relevant for pricing. Therefore:

  • ABC with β=0.60 will contribute relatively less to portfolio risk than a market-beta stock.
  • XYZ with β≈1.0 contributes market-level systematic risk.

However, the past alphas are not reliable predictors of future performance. The betas from the two brokerage houses differ considerably (ABC: 0.62 vs 0.71; XYZ: 1.45 vs 1.25), indicating estimation instability. The true betas for XYZ may be significantly higher than 0.97 from the 5-year regression, suggesting XYZ carries more systematic risk than initially estimated. Investors should use current beta estimates with caution.


1.2.2 CFA Problem 2

Correlation of Baker Fund with market index = 0.70. What percentage of Baker Fund’s total risk is nonsystematic (specific)?

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

R² = 0.49 means 49% of total variance is systematic (explained by the market).

Therefore, nonsystematic (specific) risk = 1 − 0.49 = 0.51 = 51% of total variance.


1.2.3 CFA Problem 3

Given: - Correlation of Charlottesville International Fund with world market = 1.0 - Expected return on world market index = 11% - Expected return on Charlottesville International = 9% - Risk-free rate = 3%

Find implied beta.

When correlation = 1.0, the fund moves perfectly with the market. Using the CAPM Security Market Line:

\[E(r) = r_f + \beta[E(r_M) - r_f]\] \[9\% = 3\% + \beta(11\% - 3\%)\] \[6\% = \beta \times 8\%\] \[\beta = \frac{6\%}{8\%} = 0.75\]

The implied beta of Charlottesville International is 0.75.


1.2.4 CFA Problem 4

The concept of beta is most closely associated with:

Answer: d. Systematic risk.

Beta measures the sensitivity of a security’s return to the return of the overall market (systematic/market risk). It does not measure total risk, unsystematic risk, or correlation coefficients directly.


1.2.5 CFA Problem 5

Beta and standard deviation differ as risk measures in that beta measures:

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

  • Beta captures only the systematic (non-diversifiable, market) component of risk — the co-movement with the market portfolio.
  • Standard deviation measures total risk = systematic risk + unsystematic (firm-specific) risk.

In a diversified portfolio context, only systematic risk (beta) is compensated by the market; unsystematic risk is eliminated through diversification.


1.3 Chapter 9

Reference data for CFA Problems 8 and 9:

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

(Assume risk-free rate can be inferred from the SML.)


1.3.1 CFA Problem 8

When plotting portfolio R relative to the SML, portfolio R lies:

Using the S&P 500 as the market: the SML gives E(r) for any beta. We need to find the risk-free rate. Using the market: 14% = rf + 1.0 × (14% − rf), which is always satisfied. We need another anchor. Assume rf is implied.

Using the SML: E(r_R) = rf + 0.5 × (14% − rf) = rf + 7% − 0.5rf = 0.5rf + 7%

Portfolio R’s actual return = 11%.

For R to be on the SML: 11% = 0.5rf + 7% → rf = 8%

If rf < 8%, then SML predicts E(r_R) > 11%, meaning R lies below the SML. If rf > 8%, R lies above the SML.

Typical assumption: rf is around 6% (common in these problems). With rf = 6%: SML prediction for R = 6% + 0.5(14% − 6%) = 6% + 4% = 10%. Actual return = 11% > 10%.

Answer: c. Above the SML. (Portfolio R earned more than predicted by CAPM given its beta of 0.5, indicating positive alpha.)


1.3.2 CFA Problem 9

When plotting portfolio R relative to the Capital Market Line (CML), portfolio R lies:

The CML plots expected return vs. total risk (standard deviation) for efficient portfolios. Portfolio R has σ = 10% vs. S&P 500’s σ = 12%.

CML equation: E(r) = rf + [(14% − rf)/12%] × σ

With rf = 6%: CML at σ=10%: E(r) = 6% + (8%/12%) × 10% = 6% + 6.67% = 12.67%

Portfolio R’s actual return = 11% < 12.67%.

Answer: b. Below the CML.

Portfolio R is not mean-variance efficient (it does not lie on the CML), earning less than an efficient portfolio with the same total risk. This is consistent with R having a low beta (0.5) and therefore low total return despite its 10% standard deviation — much of its risk is unsystematic.


1.3.3 CFA Problem 10

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

Portfolio A Portfolio B
Systematic risk (beta) 1.0 1.0
Specific (unsystematic) risk High Low

Answer: No. According to CAPM, investors should NOT expect a higher return on Portfolio A.

CAPM states that only systematic risk (beta) is priced and compensated in equilibrium. Both portfolios have identical betas of 1.0, so CAPM predicts the same expected return for both.

Unsystematic (specific) risk can be eliminated through diversification and therefore is not compensated by the market. Portfolio A’s higher specific risk represents inefficiency that rational, diversified investors would not be paid extra to bear. Investors holding Portfolio A should diversify to eliminate the excess unsystematic risk rather than expect a return premium for it.


1.4 Chapter 10

Context: McCracken uses a two-factor APT model where the factors are changes in real GDP (factor risk premium = 8%) and changes in inflation (factor risk premium = 2%). Risk-free rate = 4%.

Orb’s High Growth Fund: sensitivities β_GDP = 1.25, β_inflation = 1.5
Orb’s Large Cap Fund: Kwon estimates expected return = 8.5% above risk-free rate; sensitivities β_GDP = 0.75, β_inflation = 1.25
Orb’s Utility Fund: sensitivities β_GDP = 1.0, β_inflation = 2.0


1.4.1 Problem 13

According to APT, if risk-free rate = 4%, what is McCracken’s estimate of the expected return of Orb’s High Growth Fund?

\[E(r) = r_f + \beta_{GDP} \times RP_{GDP} + \beta_{inflation} \times RP_{inflation}\] \[E(r) = 4\% + 1.25 \times 8\% + 1.5 \times 2\%\] \[E(r) = 4\% + 10\% + 3\% = \mathbf{17\%}\]

The APT expected return for Orb’s High Growth Fund is 17%.


1.4.2 Problem 14

Is there an arbitrage opportunity available for the Large Cap Fund?

First, compute APT equilibrium expected return for Large Cap Fund:

\[E(r)_{APT} = 4\% + 0.75 \times 8\% + 1.25 \times 2\% = 4\% + 6\% + 2.5\% = 12.5\%\]

Kwon’s fundamental analysis estimate: rf + 8.5% = 4% + 8.5% = 12.5%

The fundamental analysis return (12.5%) equals the APT model return (12.5%).

No arbitrage opportunity exists. The Large Cap Fund is correctly priced according to the APT model — there is no mispricing to exploit.


1.4.3 Problem 15

If the GDP Fund is constructed from the three funds, which is its weight in the Utility Fund?

The GDP Fund must have: β_GDP = 1, β_inflation = 0.

Let w_H = weight in High Growth, w_L = weight in Large Cap, w_U = weight in Utility Fund. Weights sum to 1: w_H + w_L + w_U = 1.

GDP sensitivity equation: \[1.25 w_H + 0.75 w_L + 1.0 w_U = 1\]

Inflation sensitivity equation: \[1.5 w_H + 1.25 w_L + 2.0 w_U = 0\]

From the constraint: w_L = 1 − w_H − w_U

Substituting into GDP equation: \[1.25 w_H + 0.75(1 - w_H - w_U) + w_U = 1\] \[1.25 w_H + 0.75 - 0.75 w_H - 0.75 w_U + w_U = 1\] \[0.5 w_H + 0.25 w_U = 0.25 \quad \Rightarrow \quad 2w_H + w_U = 1 \quad \cdots (I)\]

Substituting into inflation equation: \[1.5 w_H + 1.25(1 - w_H - w_U) + 2 w_U = 0\] \[1.5 w_H + 1.25 - 1.25 w_H - 1.25 w_U + 2 w_U = 0\] \[0.25 w_H + 0.75 w_U = -1.25 \quad \Rightarrow \quad w_H + 3 w_U = -5 \quad \cdots (II)\]

From (I): w_H = (1 − w_U)/2. Substitute into (II): \[(1 - w_U)/2 + 3 w_U = -5\] \[1 - w_U + 6 w_U = -10\] \[5 w_U = -11\] \[w_U = -2.2\]

Answer: (a) −2.2. The weight in the Utility Fund is −2.2.

(w_H = (1−(−2.2))/2 = 1.6; w_L = 1 − 1.6 − (−2.2) = 1.6; verify: GDP: 1.25(1.6)+0.75(1.6)+1.0(−2.2) = 2.0+1.2−2.2 = 1.0 ✓; Inflation: 1.5(1.6)+1.25(1.6)+2.0(−2.2) = 2.4+2.0−4.4 = 0 ✓)


1.4.4 Problem 16

With respect to the comments of Stiles and McCracken concerning for whom the GDP Fund would be appropriate:

  • Stiles says the GDP Fund is good for retirees who live off the steady income of their investments (because it is hedged against inflation).
  • McCracken says the fund would be good if upcoming supply-side macroeconomic policies are successful (i.e., it has positive GDP exposure).

Answer: b. Both are correct.

The GDP Fund has unit sensitivity to real GDP growth and zero sensitivity to inflation. Stiles is correct that retirees concerned about inflation eroding their income would benefit — the fund is completely hedged against inflation shocks. McCracken is also correct that the fund benefits specifically when real GDP growth is strong (positive unit exposure to GDP), making it attractive if pro-growth policies succeed. Both arguments are valid from different angles.

2 Questions Using R Codes (40%)

2.1 Q1: Import Data

library(purrr)

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

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

# Extract adjusted closing prices in wide xts format
prices_xts <- prices_raw %>%
  select(date, symbol, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted) %>%
  arrange(date) %>%
  tk_xts(date_var = date)

head(prices_xts)
##                 SPY      QQQ      EEM      IWM      EFA      TLT      IYR
## 2010-01-04 84.79637 40.29078 30.35150 51.36656 35.12844 55.70953 26.76812
## 2010-01-05 85.02085 40.29078 30.57181 51.18995 35.15940 56.06931 26.83239
## 2010-01-06 85.08070 40.04777 30.63577 51.14177 35.30801 55.31876 26.82069
## 2010-01-07 85.43983 40.07380 30.45810 51.51909 35.17178 55.41179 27.06028
## 2010-01-08 85.72420 40.40362 30.69973 51.80011 35.45044 55.38697 26.87913
## 2010-01-11 85.84391 40.23871 30.63577 51.59135 35.74146 55.08302 27.00769
##               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(prices_xts)
##               SPY    QQQ   EEM    IWM    EFA   TLT    IYR    GLD
## 2026-06-02 759.57 746.16 70.80 291.66 105.02 85.65  99.99 411.95
## 2026-06-03 754.24 744.21 69.92 287.67 104.12 85.31 100.00 407.87
## 2026-06-04 757.09 740.61 69.10 292.01 104.95 85.50 101.79 411.27
## 2026-06-05 737.55 705.06 64.59 281.65 102.26 85.06 102.54 396.24
## 2026-06-08 739.22 716.07 65.75 284.11 102.88 84.62 101.08 397.27
## 2026-06-09     NA     NA    NA     NA     NA    NA     NA     NA

2.2 Q2: Calculate Weekly and Monthly Returns (Simple Returns)

# Weekly simple returns
weekly_returns <- prices_xts %>%
  apply.weekly(function(x) x[nrow(x),]) %>%  # end-of-week prices
  CalculateReturns(method = "simple") %>%
  na.omit()

# Monthly simple returns
monthly_returns <- prices_xts %>%
  apply.monthly(function(x) x[nrow(x),]) %>%  # end-of-month prices
  CalculateReturns(method = "simple") %>%
  na.omit()

cat("Weekly returns (first 3 rows):\n")
## Weekly returns (first 3 rows):
head(weekly_returns, 3)
##                     SPY         QQQ         EEM         IWM          EFA
## 2010-01-15 -0.008117805 -0.01503715 -0.02893535 -0.01301943 -0.003493553
## 2010-01-22 -0.038982111 -0.03685928 -0.05578098 -0.03062170 -0.055740562
## 2010-01-29 -0.016665700 -0.03102359 -0.03357721 -0.02624321 -0.025802740
##                    TLT          IYR          GLD
## 2010-01-15 0.020047024 -0.006304459 -0.004579349
## 2010-01-22 0.010101393 -0.041785012 -0.033285246
## 2010-01-29 0.003369243 -0.008447863 -0.011290465
cat("\nMonthly returns (first 3 rows):\n")
## 
## Monthly returns (first 3 rows):
head(monthly_returns, 3)
##                   SPY        QQQ          EEM        IWM         EFA
## 2010-02-26 0.03119470 0.04603847  0.017763773 0.04475128  0.00266762
## 2010-03-31 0.06087967 0.07710898  0.081108578 0.08230707  0.06385420
## 2010-04-30 0.01547024 0.02242518 -0.001661622 0.05678419 -0.02804588
##                     TLT        IYR          GLD
## 2010-02-26 -0.003425016 0.05457042  0.032748219
## 2010-03-31 -0.020572990 0.09748489 -0.004386396
## 2010-04-30  0.033218260 0.06388117  0.058834363

2.3 Q3: Convert Monthly Returns to Tibble Format

library(tidyverse)
library(magrittr)

monthly_ret_tbl <- monthly_returns %>%
  tk_tbl(rename_index = "date") %>%
  mutate(date = as.yearmon(date))

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

2.4 Q4: Download Fama-French 3 Factors Data

library(frenchdata)

# Download FF3 monthly factors
ff3_raw <- download_french_data("Fama/French 3 Factors")
ff3_monthly <- ff3_raw$subsets$data[[1]]  # Monthly data

ff3_tbl <- ff3_monthly %>%
  mutate(date = as.yearmon(as.character(date), "%Y%m")) %>%
  filter(date >= as.yearmon("2010-01") & date <= as.yearmon(format(Sys.Date(), "%Y-%m"))) %>%
  mutate(across(c(`Mkt-RF`, SMB, HML, RF), ~ as.numeric(.) / 100))  # Convert from % to decimal

head(ff3_tbl)
## # A tibble: 6 × 5
##   date      `Mkt-RF`     SMB     HML     RF
##   <yearmon>    <dbl>   <dbl>   <dbl>  <dbl>
## 1 Jan 2010   -0.0335  0.0043  0.0033 0     
## 2 Feb 2010    0.0339  0.0118  0.0318 0     
## 3 Mar 2010    0.063   0.0146  0.0219 0.0001
## 4 Apr 2010    0.0199  0.0484  0.0296 0.0001
## 5 May 2010   -0.079   0.0013 -0.0248 0.0001
## 6 Jun 2010   -0.0556 -0.0179 -0.0473 0.0001
tail(ff3_tbl)
## # A tibble: 6 × 5
##   date      `Mkt-RF`     SMB     HML     RF
##   <yearmon>    <dbl>   <dbl>   <dbl>  <dbl>
## 1 Nov 2025   -0.0013  0.0054  0.0357 0.003 
## 2 Dec 2025   -0.0036 -0.0103  0.0236 0.0034
## 3 Jan 2026    0.0103  0.0212  0.0386 0.003 
## 4 Feb 2026   -0.0117  0.0024  0.0265 0.0028
## 5 Mar 2026   -0.0518  0.0044  0.0335 0.0029
## 6 Apr 2026    0.0994  0.0013 -0.0127 0.0029

2.5 Q5: Merge Monthly Returns with FF3 Factors

merged_tbl <- monthly_ret_tbl %>%
  inner_join(ff3_tbl, by = "date") %>%
  arrange(date)

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

2.6 Q6: CAPM Covariance Matrix & GMV Portfolio (2015/01)

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

# Filter training window 2010/02 - 2015/01
train_data <- merged_tbl %>%
  filter(date >= as.yearmon("2010-02") & date <= as.yearmon("2015-01"))

# Excess returns for CAPM regression
asset_cols <- tickers
train_excess <- train_data %>%
  mutate(across(all_of(asset_cols), ~ . - RF))

# CAPM: regress each asset excess return on Mkt-RF, get residuals
# Covariance matrix = beta*beta' * var(Mkt) + diag(residual variances)
mkt_var <- var(train_data$`Mkt-RF`)
betas <- sapply(asset_cols, function(tk) {
  fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF`")), data = train_data)
  coef(fit)[2]
})
resid_vars <- sapply(asset_cols, function(tk) {
  fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF`")), data = train_data)
  var(resid(fit))
})

cov_capm <- outer(betas, betas) * mkt_var + diag(resid_vars)
rownames(cov_capm) <- colnames(cov_capm) <- asset_cols

cat("CAPM Covariance Matrix (2010/02-2015/01):\n")
## CAPM Covariance Matrix (2010/02-2015/01):
round(cov_capm, 6)
##           SPY       QQQ       EEM       IWM       EFA       TLT       IYR
## SPY  0.001397  0.001459  0.001726  0.001828  0.001581 -0.001011  0.001179
## QQQ  0.001459  0.001816  0.001815  0.001923  0.001662 -0.001064  0.001240
## EEM  0.001726  0.001815  0.003350  0.002274  0.001966 -0.001258  0.001466
## IWM  0.001828  0.001923  0.002274  0.002699  0.002083 -0.001333  0.001554
## EFA  0.001581  0.001662  0.001966  0.002083  0.002413 -0.001152  0.001343
## TLT -0.001011 -0.001064 -0.001258 -0.001333 -0.001152  0.001621 -0.000859
## IYR  0.001179  0.001240  0.001466  0.001554  0.001343 -0.000859  0.002025
## GLD  0.000235  0.000247  0.000292  0.000310  0.000268 -0.000171  0.000200
##           GLD
## SPY  0.000235
## QQQ  0.000247
## EEM  0.000292
## IWM  0.000310
## EFA  0.000268
## TLT -0.000171
## IYR  0.000200
## GLD  0.002899
# GMV weights
w_capm <- gmv_weights(cov_capm)
names(w_capm) <- asset_cols

cat("\nGMV Weights (CAPM):\n")
## 
## GMV Weights (CAPM):
round(w_capm, 4)
##    SPY    QQQ    EEM    IWM    EFA    TLT    IYR    GLD 
## 0.4471 0.0000 0.0000 0.0000 0.0000 0.4483 0.0373 0.0673
# Realized return 2015/02
ret_201502 <- merged_tbl %>% filter(date == as.yearmon("2015-02"))
realized_capm <- sum(w_capm * as.numeric(ret_201502[asset_cols]))
cat("\nRealized GMV Portfolio Return (CAPM) on 2015/02:", round(realized_capm * 100, 4), "%\n")
## 
## Realized GMV Portfolio Return (CAPM) on 2015/02: -0.7329 %

2.7 Q7: FF3 Covariance Matrix & GMV Portfolio (2015/01)

# FF3: regress each asset excess return on Mkt-RF, SMB, HML
# Cov = B * Cov(factors) * B' + diag(residual variances)
factor_cols <- c("Mkt-RF", "SMB", "HML")
factor_cov  <- cov(train_data[, factor_cols])

B_mat <- sapply(asset_cols, function(tk) {
  fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF` + SMB + HML")), data = train_data)
  coef(fit)[2:4]
})  # 3 x 8 matrix of factor loadings

resid_vars_ff3 <- sapply(asset_cols, function(tk) {
  fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF` + SMB + HML")), data = train_data)
  var(resid(fit))
})

cov_ff3 <- t(B_mat) %*% factor_cov %*% B_mat + diag(resid_vars_ff3)
rownames(cov_ff3) <- colnames(cov_ff3) <- asset_cols

cat("FF3 Covariance Matrix (2010/02-2015/01):\n")
## FF3 Covariance Matrix (2010/02-2015/01):
round(cov_ff3, 6)
##           SPY       QQQ       EEM       IWM       EFA       TLT       IYR
## SPY  0.001397  0.001464  0.001725  0.001787  0.001601 -0.001008  0.001179
## QQQ  0.001464  0.001816  0.001844  0.001870  0.001713 -0.000995  0.001248
## EEM  0.001725  0.001844  0.003350  0.002270  0.001980 -0.001228  0.001470
## IWM  0.001787  0.001870  0.002270  0.002699  0.001943 -0.001379  0.001552
## EFA  0.001601  0.001713  0.001980  0.001943  0.002413 -0.001104  0.001347
## TLT -0.001008 -0.000995 -0.001228 -0.001379 -0.001104  0.001621 -0.000851
## IYR  0.001179  0.001248  0.001470  0.001552  0.001347 -0.000851  0.002025
## GLD  0.000204  0.000337  0.000350  0.000469  0.000237 -0.000072  0.000216
##           GLD
## SPY  0.000204
## QQQ  0.000337
## EEM  0.000350
## IWM  0.000469
## EFA  0.000237
## TLT -0.000072
## IYR  0.000216
## GLD  0.002899
# GMV weights
w_ff3 <- gmv_weights(cov_ff3)
names(w_ff3) <- asset_cols

cat("\nGMV Weights (FF3):\n")
## 
## GMV Weights (FF3):
round(w_ff3, 4)
##    SPY    QQQ    EEM    IWM    EFA    TLT    IYR    GLD 
## 0.4579 0.0000 0.0000 0.0000 0.0000 0.4507 0.0334 0.0581
# Realized return 2015/02
realized_ff3 <- sum(w_ff3 * as.numeric(ret_201502[asset_cols]))
cat("\nRealized GMV Portfolio Return (FF3) on 2015/02:", round(realized_ff3 * 100, 4), "%\n")
## 
## Realized GMV Portfolio Return (FF3) on 2015/02: -0.6224 %

2.8 Q8: Backtesting GMV Portfolios (2015/02 – 2026/05)

all_dates <- merged_tbl$date
out_dates  <- all_dates[all_dates >= as.yearmon("2015-02") & all_dates <= as.yearmon("2026-05")]

backtest_returns <- tibble(date = out_dates, capm = NA_real_, ff3 = NA_real_)

for (d in seq_along(out_dates)) {
  t_date   <- out_dates[d]
  t_end    <- all_dates[all_dates < t_date]
  t_start  <- tail(t_end, 60)[1]
  if (is.na(t_start)) next

  win <- merged_tbl %>% filter(date >= t_start & date < t_date)
  if (nrow(win) < 30) next

  # CAPM covariance
  mkt_v <- var(win$`Mkt-RF`)
  betas_w <- sapply(asset_cols, function(tk) {
    fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF`")), data = win)
    coef(fit)[2]
  })
  rv_w <- sapply(asset_cols, function(tk) {
    fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF`")), data = win)
    var(resid(fit))
  })
  cov_c <- outer(betas_w, betas_w) * mkt_v + diag(rv_w)

  # FF3 covariance
  fc <- cov(win[, factor_cols])
  B_w <- sapply(asset_cols, function(tk) {
    fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF` + SMB + HML")), data = win)
    coef(fit)[2:4]
  })
  rv_ff_w <- sapply(asset_cols, function(tk) {
    fit <- lm(as.formula(paste0("`", tk, "` - RF ~ `Mkt-RF` + SMB + HML")), data = win)
    var(resid(fit))
  })
  cov_f <- t(B_w) %*% fc %*% B_w + diag(rv_ff_w)

  # GMV weights
  tryCatch({
    wc <- gmv_weights(cov_c)
    wf <- gmv_weights(cov_f)
    row_t <- merged_tbl %>% filter(date == t_date)
    backtest_returns$capm[d] <- sum(wc * as.numeric(row_t[asset_cols]))
    backtest_returns$ff3[d]  <- sum(wf * as.numeric(row_t[asset_cols]))
  }, error = function(e) NULL)
}

# Cumulative returns
cum_ret <- backtest_returns %>%
  na.omit() %>%
  mutate(
    cum_capm = cumprod(1 + capm),
    cum_ff3  = cumprod(1 + ff3)
  )

# Plot
cum_ret %>%
  select(date, cum_capm, cum_ff3) %>%
  pivot_longer(-date, names_to = "model", values_to = "cum_return") %>%
  mutate(model = recode(model,
                        "cum_capm" = "GMV (CAPM)",
                        "cum_ff3"  = "GMV (FF3)")) %>%
  ggplot(aes(x = as.Date(date), y = cum_return, color = model)) +
  geom_line(linewidth = 1.2) +
  scale_color_manual(values = c("GMV (CAPM)" = "#2196F3", "GMV (FF3)" = "#F44336")) +
  scale_y_continuous(labels = scales::percent_format(scale = 100)) +
  labs(
    title    = "Cumulative Returns: GMV Portfolios (2015/02 – 2026/05)",
    subtitle = "Rolling 60-month window; CAPM vs. Fama-French 3-Factor covariance",
    x        = "Date",
    y        = "Cumulative Return",
    color    = "Model",
    caption  = "Assets: SPY, QQQ, EEM, IWM, EFA, TLT, IYR, GLD"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom")

# Summary statistics
cat("\n=== Backtest Summary ===\n")
## 
## === Backtest Summary ===
cum_ret %>%
  summarise(
    CAPM_Total_Return  = scales::percent(last(cum_capm) - 1, accuracy = 0.01),
    FF3_Total_Return   = scales::percent(last(cum_ff3) - 1, accuracy = 0.01),
    CAPM_Ann_Return    = scales::percent((last(cum_capm))^(12/n()) - 1, accuracy = 0.01),
    FF3_Ann_Return     = scales::percent((last(cum_ff3))^(12/n()) - 1, accuracy = 0.01),
    CAPM_Ann_Vol       = scales::percent(sd(capm) * sqrt(12), accuracy = 0.01),
    FF3_Ann_Vol        = scales::percent(sd(ff3) * sqrt(12), accuracy = 0.01)
  ) %>%
  pivot_longer(everything(), names_to = "Statistic", values_to = "Value") %>%
  knitr::kable()
Statistic Value
CAPM_Total_Return 132.28%
FF3_Total_Return 129.49%
CAPM_Ann_Return 7.78%
FF3_Ann_Return 7.66%
CAPM_Ann_Vol 10.49%
FF3_Ann_Vol 10.57%