1 Part 1: Questions from Textbook (60%)


1.1 Chapter 7: Optimal Risky Portfolios

Context for CFA Problems 1–3: Hennessy & Associates manages a $30 million equity portfolio for the multimanager Wilstead Pension Fund. Jason Jones, VP, proposed limiting the Hennessy portfolio to no more than 20 stocks (from 40), doubling commitments to the favored stocks. The portfolio historically held ~40 stocks at 2–3% each, identifying ~10 large-gain issues per year.


1.1.1 CFA Problem 1

Q1a: Will limiting to 20 stocks likely increase or decrease the risk of the portfolio? Explain.

Answer: Limiting the portfolio to 20 stocks will increase unsystematic (idiosyncratic) risk. Diversification benefits diminish as the number of holdings decreases. Moving from 40 to 20 stocks means fewer independent return streams to offset each other, so stock-specific shocks will have a greater impact on portfolio variance.

However, the relevant question is whether total portfolio risk increases. If Hennessy selects the 20 highest-conviction (and potentially highly correlated) stocks, systematic risk could also rise. In general, risk will increase.

Q1b: Is there any way Hennessy could reduce from 40 to 20 stocks without significantly affecting risk? Explain.

Answer: Yes — if Hennessy selects 20 stocks that are relatively low in correlation with each other and represent different sectors or risk factors, the reduction in unsystematic risk from correlation effects could partially offset the reduction in the number of holdings. In the extreme, if the 20 remaining stocks are nearly uncorrelated, portfolio variance could remain similar. Additionally, if the 20 eliminated stocks were very similar to (highly correlated with) the retained ones, their removal adds little incremental diversification loss.


1.1.2 CFA Problem 2

Q: If reduction from 40 to 20 is expected to be advantageous, explain why a further reduction to 10 might be less advantageous.

Answer: The marginal diversification benefit from adding stocks diminishes as portfolio size increases. Going from 40 → 20 stocks roughly doubles the concentration per position but may still leave substantial diversification. However, going from 20 → 10 stocks is a proportionally larger concentration step relative to the base, and the law of diminishing diversification means each additional stock removed at a smaller base causes a larger increase in idiosyncratic risk.

Mathematically, if returns are uncorrelated with variance \(\sigma^2\), the portfolio variance of an equal-weight \(n\)-stock portfolio is \(\sigma^2/n\). Variance doubles going from 20→10, but only increases 33% going from 40→30. So the 20→10 reduction is proportionally much more harmful.

Furthermore, concentrating in only 10 stocks makes the portfolio highly sensitive to single-stock events and manager skill. (Note: the problem states Wilstead evaluates Hennessy independently of other portfolios.)


1.1.3 CFA Problem 3

Q: If the Hennessy portfolio is evaluated in the context of the total Wilstead fund (rather than independently), how does this affect the decision to limit to 10 or 20 stocks?

Answer: Viewing Hennessy’s portfolio as a component of a larger fund changes the analysis significantly. The relevant risk measure for each component is its marginal contribution to total fund risk — i.e., its covariance with the rest of the fund — rather than its standalone variance.

The other five managers collectively hold 150+ stocks across $250 million. Hennessy’s 40 stocks therefore represent a small, diversified slice of the total fund. In this context:

  • Even a highly concentrated Hennessy portfolio (10 stocks) contributes relatively little additional risk to the total $280M fund, which is already broadly diversified.
  • The committee should therefore be less concerned about the reduction from 40 to 10 or 20 stocks, because the total fund diversification absorbs Hennessy’s idiosyncratic risk.
  • The decision should focus on whether Hennessy’s alpha-generating skill (stock selection) is best exploited at 10, 20, or 40 stocks — and since the total fund is well-diversified, a higher-conviction, more concentrated Hennessy portfolio may be optimal.

1.1.4 CFA Problem 4

Q: Which portfolio 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.

Reasoning: On the Markowitz efficient frontier, no portfolio should offer a lower return per unit of risk than another that is clearly dominated. We compare reward-to-risk (using return/SD as a crude measure):

  • W: 15/36 = 0.417
  • X: 12/15 = 0.800
  • Z: 5/7 = 0.714
  • Y: 9/21 = 0.429

More rigorously, portfolio W (15% return, 36% SD) is dominated: by accepting higher variance than X (which has 12% return, 15% SD), an investor could simply leverage up portfolio X to achieve 15% return at far lower risk than 36%.

Specifically, lending/borrowing at the risk-free rate and investing in a high-Sharpe-ratio portfolio will dominate W. Portfolio W, with a higher standard deviation relative to its return compared to the other portfolios, lies below the efficient frontier — it offers insufficient expected return for its level of risk.


1.1.5 CFA Problem 10

Q: Given stocks A, B, and C with the following statistics, which 50/50 portfolio — (A+B) or (B+C) — would you recommend?

Stock Std Dev (%)
A 40
B 20
C 40

Correlation matrix:

A B C
A 1.00 0.90 0.50
B 1.00 0.10
C 1.00

Answer: Recommend the (B+C) portfolio.

Calculation of portfolio variance for equal-weight (50/50) portfolios:

\[\sigma_p^2 = w_1^2\sigma_1^2 + w_2^2\sigma_2^2 + 2w_1 w_2 \rho_{12}\sigma_1\sigma_2\]

Portfolio A+B:

\[\sigma_{AB}^2 = (0.5)^2(40)^2 + (0.5)^2(20)^2 + 2(0.5)(0.5)(0.90)(40)(20)\] \[= 400 + 100 + 720 = 1220\] \[\sigma_{AB} = \sqrt{1220} \approx 34.93\%\]

Portfolio B+C:

\[\sigma_{BC}^2 = (0.5)^2(20)^2 + (0.5)^2(40)^2 + 2(0.5)(0.5)(0.10)(20)(40)\] \[= 100 + 400 + 80 = 580\] \[\sigma_{BC} = \sqrt{580} \approx 24.08\%\]

Since expected returns are not given, we can only compare risk. The B+C portfolio has substantially lower variance (580 vs. 1220) due to the much lower correlation (0.10 vs. 0.90). The diversification benefit is far greater in B+C. Therefore, (B+C) is preferred on a risk-minimization basis.


1.2 Chapter 8: Index Models


1.2.1 CFA Problem 1

Q: The following regression results were obtained for ABC and XYZ stocks. Explain risk-return implications and comment on future relationships.

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

Additional brokerage data (most recent 2 years, weekly returns):

Brokerage House Beta of ABC Beta of XYZ
A 0.62 1.45
B 0.71 1.25

Answer:

ABC: - Alpha = −3.20%: Over the sample period, ABC underperformed its CAPM-predicted return by 3.2% annually — negative abnormal return. - Beta = 0.60: ABC has below-market systematic risk; it moves less than the market. - R² = 0.35: Only 35% of ABC’s return variation is explained by market movements. Most risk is firm-specific (unsystematic). - Residual SD = 13.02%: Substantial idiosyncratic risk remains.

XYZ: - Alpha = +7.30%: XYZ earned 7.3% excess return above CAPM prediction — attractive historical performance. - Beta = 0.97: Near-market systematic risk. - R² = 0.17: Only 17% of XYZ variation is market-driven. Very high unsystematic risk. - Residual SD = 21.45%: Very large idiosyncratic risk.

Implications for a diversified portfolio: When stocks are held in a well-diversified portfolio, unsystematic risk is eliminated. The relevant risk measure becomes beta. Both ABC (β=0.60) and XYZ (β=0.97) contribute modest systematic risk. However, the large residual variances suggest significant specific risk that diversification can remove.

Future beta estimates: The two brokerage houses give notably different betas for XYZ (1.45 vs. 1.25) and slightly different for ABC. This suggests XYZ’s beta is unstable or sensitive to estimation period/frequency. Practitioners typically use a Blume-adjusted or Vasicek-adjusted beta to account for mean-reversion, or take an average across sources. ABC’s beta appears more stable (0.62 vs. 0.71 vs. 0.60 in the 5-year regression).


1.2.2 CFA Problem 2

Q: The correlation coefficient between Baker Fund and the market index is 0.70. What percentage of Baker Fund’s total risk is specific (nonsystematic)?

Answer:

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

So 49% of Baker Fund’s total risk (variance) is systematic (market-related).

Therefore, nonsystematic (specific) risk = 1 − R² = 1 − 0.49 = 0.51 = 51%.


1.2.3 CFA Problem 3

Q: Charlottesville International Fund has a correlation of 1.0 with a broad world index. Expected world return = 11%, expected fund return = 9%, risk-free rate = 3%. What is the implied beta?

Answer:

Since the correlation is 1.0, the fund moves perfectly with the world market. Using CAPM:

\[E(R_i) = R_f + \beta_i[E(R_M) - R_f]\]

\[9\% = 3\% + \beta \times (11\% - 3\%)\]

\[6\% = \beta \times 8\%\]

\[\beta = \frac{6\%}{8\%} = \mathbf{0.75}\]

The implied beta of Charlottesville International is 0.75.


1.2.4 CFA Problem 4

Q: The concept of beta is most closely associated with:

  1. Correlation coefficients
  2. Mean-variance analysis
  3. Nonsystematic risk
    d. Systematic risk

Answer: (d) Systematic risk.

Beta measures an asset’s sensitivity to market-wide (systematic) movements. It captures the component of return variance that cannot be diversified away and represents the asset’s contribution to portfolio systematic risk.


1.2.5 CFA Problem 5

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

  1. Only unsystematic risk, while standard deviation measures total risk
    b. Only systematic risk, while standard deviation measures total risk
  2. Both systematic and unsystematic, while SD measures only unsystematic
  3. Both systematic and unsystematic, while SD measures only systematic

Answer: (b).

Standard deviation (or variance) captures total risk — both systematic and unsystematic. Beta measures only the systematic (market-related) component. In a diversified portfolio, only systematic risk is priced; unsystematic risk is diversified away. Therefore, beta is the appropriate risk measure for a security held within a diversified portfolio.


1.3 Chapter 9: The Capital Asset Pricing Model

Context for CFA Problems 8 and 9: Risk and return data for two portfolios:

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

1.3.1 CFA Problem 8

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

  1. On the SML
  2. Below the SML
    c. Above the SML
  3. Insufficient data

Answer: (c) Above the SML.

The SML gives expected return as a function of beta:

\[E(R) = R_f + \beta[E(R_M) - R_f]\]

We need a risk-free rate. Using the S&P 500 as the market (14%, β=1.0), and assuming a reasonable \(R_f\), we can compute the SML-implied return for Portfolio R (β=0.5):

\[E(R_R)_{SML} = R_f + 0.5 \times (14\% - R_f) = \frac{R_f}{2} + 7\%\]

For example, if \(R_f = 6\%\): \(E(R_R)_{SML} = 3\% + 7\% = 10\%\). Since Portfolio R actually earns 11% > 10%, it plots above the SML (positive alpha). This result holds for any reasonable risk-free rate, confirming that R earns a positive abnormal return relative to its systematic risk.


1.3.2 CFA Problem 9

Q: When plotting portfolio R relative to the CML, portfolio R lies:

  1. On the CML
    b. Below the CML
  2. Above the CML
  3. Insufficient data

Answer: (b) Below the CML.

The CML plots efficient portfolios (combinations of risk-free asset and market portfolio) by total risk (standard deviation). Portfolio R has: - Return: 11%, SD: 10% - S&P 500: Return: 14%, SD: 12%

The CML at SD = 10%:

\[E(R_{CML}) = R_f + \frac{14\% - R_f}{12\%} \times 10\%\]

With \(R_f = 6\%\): \(E(R_{CML}) = 6\% + (8\%/12\%) \times 10\% = 6\% + 6.67\% = 12.67\%\).

Since Portfolio R earns only 11% vs. 12.67% on the CML at the same SD, R lies below the CML. Portfolio R is not an efficient (fully diversified) portfolio. Its relatively high total risk for its return (compared to CML) indicates the presence of undiversified idiosyncratic risk.


1.3.3 CFA Problem 10

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

Measure Portfolio A Portfolio B
Systematic risk (beta) 1.0 1.0
Specific risk (each security) High Low

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

CAPM holds that only systematic risk (beta) is priced in equilibrium, because unsystematic risk can be freely diversified away. Both portfolios have identical betas (1.0), so CAPM predicts the same expected return for both:

\[E(R_A) = E(R_B) = R_f + 1.0 \times [E(R_M) - R_f]\]

Portfolio A has higher specific (idiosyncratic) risk per security, but rational investors holding diversified portfolios will not demand compensation for risk they can eliminate without cost. In the CAPM world, no return premium is earned for bearing avoidable unsystematic risk.


1.4 Chapter 10: Arbitrage Pricing Theory

Context for Problems 13–16 (Orb Trust / McCracken APT):

Two-factor APT model with factors: - Factor 1: Real GDP growth — risk premium = 8% - Factor 2: Inflation changes — risk premium = 2%

High Growth Fund: \(b_1 = 1.25\), \(b_2 = 1.5\)

Large Cap Fund: Kwon estimates expected return = \(R_f + 8.5\%\); McCracken finds \(b_1 = 0.75\), \(b_2 = 1.25\)

Utility Fund: \(b_1 = 1.0\), \(b_2 = 2.0\) (used to construct GDP Fund)


1.4.1 Problem 13

Q: If the risk-free rate is 4%, what is McCracken’s APT estimate of the expected return for the High Growth Fund?

Answer:

Using the two-factor APT:

\[E(R) = R_f + b_1 \times \lambda_1 + b_2 \times \lambda_2\]

\[E(R_{HGF}) = 4\% + 1.25 \times 8\% + 1.5 \times 2\%\]

\[= 4\% + 10\% + 3\% = \mathbf{17\%}\]

McCracken’s APT estimate for the High Growth Fund expected return is 17%.


1.4.2 Problem 14

Q: With respect to McCracken’s APT estimate of the Large Cap Fund and Kwon’s information, is an arbitrage opportunity available?

Answer:

McCracken’s APT estimate for Large Cap Fund (\(R_f = 4\%\)):

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

Kwon’s fundamental analysis estimate:

\[E(R_{LCF})_{Kwon} = R_f + 8.5\% = 4\% + 8.5\% = 12.5\%\]

Both estimates yield 12.5%. Since the fundamental and APT-model values are equal, there is no arbitrage opportunity. The Large Cap Fund is fairly priced according to the APT.


1.4.3 Problem 15

Q: If the GDP Fund is constructed from the three funds (to have unit sensitivity to GDP and zero sensitivity to inflation), what is its weight in the Utility Fund?

Answer: (c) 0.3

We need portfolio weights \(w_{HGF}\), \(w_{LCF}\), \(w_{UF}\) (summing to 1) such that:

Factor 1 (GDP): \(1.25w_{HGF} + 0.75w_{LCF} + 1.0w_{UF} = 1\)

Factor 2 (Inflation): \(1.5w_{HGF} + 1.25w_{LCF} + 2.0w_{UF} = 0\)

Weights sum to 1: \(w_{HGF} + w_{LCF} + w_{UF} = 1\)

Solving this 3×3 system:

From the weights constraint: \(w_{LCF} = 1 - w_{HGF} - w_{UF}\)

Substituting into the inflation equation:

\(1.5w_{HGF} + 1.25(1 - w_{HGF} - w_{UF}) + 2.0w_{UF} = 0\)

\(0.25w_{HGF} + 0.75w_{UF} = -1.25\)

Substituting into the GDP equation:

\(1.25w_{HGF} + 0.75(1 - w_{HGF} - w_{UF}) + 1.0w_{UF} = 1\)

\(0.5w_{HGF} + 0.25w_{UF} = 0.25\)

From these two equations: multiplying the second by 3 and subtracting from the first:

\(0.25w_{HGF} + 0.75w_{UF} - 1.5w_{HGF} - 0.75w_{UF} = -1.25 - 0.75\)

\(-1.25w_{HGF} = -2\), so \(w_{HGF} = 1.6\)

Then \(0.5(1.6) + 0.25w_{UF} = 0.25 \Rightarrow w_{UF} = (0.25 - 0.8)/0.25 = -2.2\)

Wait — let me re-check with option (c) 0.3:

With \(w_{UF} = 0.3\): From the GDP equation: \(1.25w_{HGF} + 0.75w_{LCF} = 0.70\), and \(w_{HGF} + w_{LCF} = 0.70\).

If \(w_{HGF} = w_{LCF}\): both = 0.35. Check inflation: \(1.5(0.35)+1.25(0.35)+2.0(0.3)=0.525+0.4375+0.6=1.5625\neq0\).

Let me solve properly. Setting up the equations again:

Constraint Equation
GDP = 1 \(1.25a + 0.75b + 1.0c = 1\)
Inflation = 0 \(1.5a + 1.25b + 2.0c = 0\)
Weights = 1 \(a + b + c = 1\)

Solving: \(a = w_{HGF}\), \(b = w_{LCF}\), \(c = w_{UF}\).

From rows 1 and 3: \(0.25a - 0.25b = 0 \Rightarrow a = b\)… wait: Row1 − Row3: \(0.25a - 0.25b + 0c = 0\), so \(a = b\).

Substituting \(a = b\) into Row3: \(2a + c = 1\), so \(c = 1 - 2a\).

Into Row2: \(1.5a + 1.25a + 2(1-2a) = 0 \Rightarrow 2.75a + 2 - 4a = 0 \Rightarrow -1.25a = -2 \Rightarrow a = 1.6\).

Then \(c = 1 - 3.2 = -2.2\). This gives option (a) −2.2.

But wait — Re-reading the problem: the GDP Fund is constructed from three funds with the Utility Fund having \(b_1=1.0, b_2=2.0\). The answer choices are (a) −2.2; (b) −3.2; (c) .3.

The solution gives \(w_{UF} = -2.2\), so the answer is (a) −2.2.


1.4.4 Problem 16

Q: With respect to Stiles’s and McCracken’s comments about the GDP Fund:

  1. McCracken is correct and Stiles is wrong
    b. Both are correct
  2. Stiles is correct and McCracken is wrong

Answer: (b) Both are correct.

  • Stiles says the GDP Fund would be good for retirees who depend on steady investment income, because a GDP-linked fund provides a hedge against real economic growth risk — when the economy grows, the fund benefits, helping maintain the real value of wealth for income-dependent investors.

  • McCracken says the fund would be good if upcoming supply-side macroeconomic policies are successful. Supply-side policies aim to boost real GDP growth, which would directly lift a portfolio with high positive GDP sensitivity (unit exposure).

Both statements are logically consistent and can be simultaneously correct — they describe different scenarios/investor profiles where the GDP Fund is appropriate.


2 Part 2: Questions Using R Code (40%)


2.1 Q1: Import Data

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

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

# Download daily data 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 and pivot to wide format
prices_wide <- prices_raw %>%
  select(symbol, date, adjusted) %>%
  pivot_wider(names_from = symbol, values_from = adjusted)

# Convert to xts
prices_xts <- prices_wide %>%
  column_to_rownames("date") %>%
  as.xts()

# Show first and last rows
head(prices_xts)
##                 SPY      QQQ      EEM      IWM      EFA      TLT      IYR
## 2010-01-04 84.79639 40.29078 30.35151 51.36656 35.12844 55.70950 26.76811
## 2010-01-05 85.02084 40.29078 30.57181 51.18994 35.15940 56.06928 26.83237
## 2010-01-06 85.08071 40.04776 30.63577 51.14177 35.30800 55.31875 26.82070
## 2010-01-07 85.43984 40.07380 30.45811 51.51909 35.17178 55.41179 27.06028
## 2010-01-08 85.72418 40.40361 30.69972 51.80010 35.45043 55.38699 26.87913
## 2010-01-11 85.84389 40.23872 30.63577 51.59137 35.74147 55.08304 27.00768
##               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 returns (simple)
weekly_returns_xts <- apply.weekly(prices_xts, function(x) {
  as.numeric(last(x)) / as.numeric(first(x)) - 1
})

# Monthly returns (simple)
monthly_returns_xts <- apply.monthly(prices_xts, function(x) {
  as.numeric(last(x)) / as.numeric(first(x)) - 1
})

# Preview weekly returns
cat("Weekly Returns (first 5 rows):\n")
## Weekly Returns (first 5 rows):
head(weekly_returns_xts, 5)
##                      SPY          QQQ          EEM          IWM           EFA
## 2010-01-04  0.0000000000  0.000000000  0.000000000  0.000000000  0.0000000000
## 2010-01-11  0.0096805853 -0.001292276  0.002091907  0.007842085  0.0165552671
## 2010-01-15 -0.0001760147  0.001529036 -0.011079802  0.001887786  0.0001753964
## 2010-01-25 -0.0459756048 -0.048937756 -0.067756839 -0.047031847 -0.0534631827
## 2010-02-01 -0.0022862491 -0.024577740  0.006142929 -0.006046477 -0.0143859871
##                     TLT          IYR          GLD
## 2010-01-04  0.000000000  0.000000000  0.000000000
## 2010-01-11 -0.017589674  0.006533318  0.028714691
## 2010-01-15  0.008415897  0.005942026  0.003348744
## 2010-01-25  0.008367935 -0.053625368 -0.036226627
## 2010-02-01 -0.001505980  0.010973998  0.007344746
cat("\nMonthly Returns (first 5 rows):\n")
## 
## Monthly Returns (first 5 rows):
head(monthly_returns_xts, 5)
##                     SPY         QQQ          EEM         IWM         EFA
## 2010-01-29 -0.052413565 -0.07819883 -0.103722809 -0.06048761 -0.07491668
## 2010-02-26  0.015403710  0.03467423 -0.008903973  0.03255475 -0.01534435
## 2010-03-31  0.049975809  0.06169143  0.063099353  0.05771656  0.05562920
## 2010-04-30  0.008573766  0.02242536 -0.027071006  0.04705564 -0.04493630
## 2010-05-28 -0.091233799 -0.08672175 -0.098864755 -0.09568674 -0.11824821
##                     TLT         IYR          GLD
## 2010-01-29  0.027836929 -0.05195373 -0.034972713
## 2010-02-26  0.005704852  0.03573003  0.009967714
## 2010-03-31 -0.020144336  0.08633685 -0.004386396
## 2010-04-30  0.035750769  0.05898837  0.046254293
## 2010-05-28  0.052459754 -0.08516480  0.027218472

2.3 Q3: Convert Monthly Returns to Tibble Format

# Convert xts to tibble with 'date' as a column
monthly_returns_tbl <- tk_tbl(monthly_returns_xts, rename_index = "date")

# Ensure date column is Date type (tk_tbl may produce yearmon)
monthly_returns_tbl <- monthly_returns_tbl %>%
  mutate(date = as.Date(as.yearmon(date)))

cat("Monthly Returns in Tibble Format:\n")
## Monthly Returns in Tibble Format:
print(head(monthly_returns_tbl, 6))
## # A tibble: 6 × 9
##   date            SPY     QQQ      EEM     IWM     EFA      TLT     IYR      GLD
##   <date>        <dbl>   <dbl>    <dbl>   <dbl>   <dbl>    <dbl>   <dbl>    <dbl>
## 1 2010-01-01 -0.0524  -0.0782 -0.104   -0.0605 -0.0749  0.0278  -0.0520 -0.0350 
## 2 2010-02-01  0.0154   0.0347 -0.00890  0.0326 -0.0153  0.00570  0.0357  0.00997
## 3 2010-03-01  0.0500   0.0617  0.0631   0.0577  0.0556 -0.0201   0.0863 -0.00439
## 4 2010-04-01  0.00857  0.0224 -0.0271   0.0471 -0.0449  0.0358   0.0590  0.0463 
## 5 2010-05-01 -0.0912  -0.0867 -0.0989  -0.0957 -0.118   0.0525  -0.0852  0.0272 
## 6 2010-06-01 -0.0355  -0.0510  0.00447 -0.0486 -0.0108  0.0506  -0.0278  0.0148
cat("\nDimensions:", nrow(monthly_returns_tbl), "rows x", ncol(monthly_returns_tbl), "cols\n")
## 
## Dimensions: 198 rows x 9 cols

2.4 Q4: Download Fama-French 3-Factor Data

# Install frenchdata if needed — purpose-built CRAN package for Ken French's library
if (!requireNamespace("frenchdata", quietly = TRUE))
  install.packages("frenchdata", repos = "https://cloud.r-project.org")

library(frenchdata)

# Download FF3 monthly factors
ff_raw <- download_french_data("Fama/French 3 Factors")

# The monthly data lives in the first list element
ff_monthly_raw <- ff_raw$subsets$data[[1]]

# Convert to tibble, parse date (YYYYMM integer), scale to decimals
ff_factors_tbl <- ff_monthly_raw %>%
  rename(date_raw = date) %>%
  mutate(
    date_raw = as.character(date_raw),
    date     = as.Date(paste0(substr(date_raw, 1, 4), "-",
                              substr(date_raw, 5, 6), "-01")),
    Mkt.RF   = `Mkt-RF` / 100,
    SMB      = SMB      / 100,
    HML      = HML      / 100,
    RF       = RF       / 100
  ) %>%
  filter(date >= as.Date("2010-01-01"), !is.na(Mkt.RF)) %>%
  select(date, Mkt.RF, SMB, HML, RF)

# Convert to xts
ff_factors_xts <- ff_factors_tbl %>%
  column_to_rownames("date") %>%
  as.xts()

cat("FF3 Factors (first 6 rows, decimal form):\n")
## FF3 Factors (first 6 rows, decimal form):
print(head(ff_factors_tbl, 6))
## # A tibble: 6 × 5
##   date        Mkt.RF     SMB     HML     RF
##   <date>       <dbl>   <dbl>   <dbl>  <dbl>
## 1 2010-01-01 -0.0335  0.0043  0.0033 0     
## 2 2010-02-01  0.0339  0.0118  0.0318 0     
## 3 2010-03-01  0.063   0.0146  0.0219 0.0001
## 4 2010-04-01  0.0199  0.0484  0.0296 0.0001
## 5 2010-05-01 -0.079   0.0013 -0.0248 0.0001
## 6 2010-06-01 -0.0556 -0.0179 -0.0473 0.0001
cat("\nRows available:", nrow(ff_factors_tbl), "\n")
## 
## Rows available: 196

2.5 Q5: Merge Monthly Returns with FF3 Factors

# Merge monthly ETF returns with FF3 factors on date
merged_tbl <- monthly_returns_tbl %>%
  inner_join(ff_factors_tbl, by = "date") %>%
  arrange(date)

cat("Merged Data (Monthly Returns + FF3 Factors):\n")
## Merged Data (Monthly Returns + FF3 Factors):
print(head(merged_tbl, 6))
## # A tibble: 6 × 13
##   date            SPY     QQQ      EEM     IWM     EFA      TLT     IYR      GLD
##   <date>        <dbl>   <dbl>    <dbl>   <dbl>   <dbl>    <dbl>   <dbl>    <dbl>
## 1 2010-01-01 -0.0524  -0.0782 -0.104   -0.0605 -0.0749  0.0278  -0.0520 -0.0350 
## 2 2010-02-01  0.0154   0.0347 -0.00890  0.0326 -0.0153  0.00570  0.0357  0.00997
## 3 2010-03-01  0.0500   0.0617  0.0631   0.0577  0.0556 -0.0201   0.0863 -0.00439
## 4 2010-04-01  0.00857  0.0224 -0.0271   0.0471 -0.0449  0.0358   0.0590  0.0463 
## 5 2010-05-01 -0.0912  -0.0867 -0.0989  -0.0957 -0.118   0.0525  -0.0852  0.0272 
## 6 2010-06-01 -0.0355  -0.0510  0.00447 -0.0486 -0.0108  0.0506  -0.0278  0.0148 
## # ℹ 4 more variables: Mkt.RF <dbl>, SMB <dbl>, HML <dbl>, RF <dbl>
cat("\nDimensions:", nrow(merged_tbl), "rows x", ncol(merged_tbl), "cols\n")
## 
## Dimensions: 196 rows x 13 cols

2.6 Q6: CAPM-Based GMV Portfolio — Realized Return 2015/02

library(quadprog)

# ---- Helper: estimate CAPM covariance matrix ----
capm_cov_matrix <- function(returns_mat, mkt_excess) {
  n      <- ncol(returns_mat)
  betas  <- numeric(n)
  resvar <- numeric(n)
  
  mkt_excess <- as.numeric(mkt_excess)
  
  for (i in 1:n) {
    asset_exc <- as.numeric(returns_mat[, i])
    fit       <- lm(asset_exc ~ mkt_excess)
    betas[i]  <- coef(fit)[2]
    resvar[i] <- var(residuals(fit))
  }
  
  var_mkt   <- var(mkt_excess)
  # CAPM cov matrix: Sigma = beta %*% t(beta) * var_mkt + diag(resvar)
  Sigma <- outer(betas, betas) * var_mkt + diag(resvar)
  return(Sigma)
}

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

# ---- Training window: 2010/02 – 2015/01 ----
train_start <- as.Date("2010-02-01")
train_end   <- as.Date("2015-01-31")

train_data <- merged_tbl %>%
  filter(date >= train_start & date <= train_end)

etf_cols   <- tickers  # SPY, QQQ, EEM, IWM, EFA, TLT, IYR, GLD
returns_mat <- as.matrix(train_data[, etf_cols])

# Excess returns (asset - Rf)
rf_train    <- train_data$RF
excess_mat  <- sweep(returns_mat, 1, rf_train, "-")
mkt_excess  <- train_data$Mkt.RF

# Compute CAPM covariance matrix and GMV weights
Sigma_capm  <- capm_cov_matrix(excess_mat, mkt_excess)
w_gmv_capm  <- gmv_weights(Sigma_capm)

cat("CAPM GMV Weights (2015/01):\n")
## CAPM GMV Weights (2015/01):
names(w_gmv_capm) <- etf_cols
print(round(w_gmv_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
cat("Sum of weights:", round(sum(w_gmv_capm), 4), "\n")
## Sum of weights: 1
# ---- Realized portfolio return in 2015/02 ----
feb_2015 <- merged_tbl %>%
  filter(format(date, "%Y-%m") == "2015-02")

ret_feb2015 <- as.numeric(feb_2015[, etf_cols])
realized_capm <- sum(w_gmv_capm * ret_feb2015)

cat("\nRealized Portfolio Return (CAPM GMV) in 2015/02:",
    round(realized_capm * 100, 4), "%\n")
## 
## Realized Portfolio Return (CAPM GMV) in 2015/02: -1.2232 %

2.7 Q7: FF3-Factor-Based GMV Portfolio — Realized Return 2015/02

# ---- Helper: estimate FF3 covariance matrix ----
ff3_cov_matrix <- function(returns_mat, ff_factors_mat) {
  n      <- ncol(returns_mat)
  resvar <- numeric(n)
  betas  <- matrix(0, nrow = n, ncol = 3)
  
  for (i in 1:n) {
    asset_exc <- as.numeric(returns_mat[, i])
    fit       <- lm(asset_exc ~ ff_factors_mat[, 1] +
                                ff_factors_mat[, 2] +
                                ff_factors_mat[, 3])
    betas[i, ] <- coef(fit)[-1]
    resvar[i]  <- var(residuals(fit))
  }
  
  cov_factors <- cov(ff_factors_mat)
  # Sigma = B %*% cov_factors %*% t(B) + diag(resvar)
  Sigma <- betas %*% cov_factors %*% t(betas) + diag(resvar)
  return(Sigma)
}

# Prepare FF3 factor matrix for training window
ff_mat <- as.matrix(train_data[, c("Mkt.RF", "SMB", "HML")])

# Compute FF3 covariance matrix and GMV weights
Sigma_ff3  <- ff3_cov_matrix(excess_mat, ff_mat)
w_gmv_ff3  <- gmv_weights(Sigma_ff3)

cat("FF3 GMV Weights (2015/01):\n")
## FF3 GMV Weights (2015/01):
names(w_gmv_ff3) <- etf_cols
print(round(w_gmv_ff3, 4))
##    SPY    QQQ    EEM    IWM    EFA    TLT    IYR    GLD 
## 0.3275 0.0282 0.0000 0.0292 0.0214 0.4689 0.0641 0.0608
cat("Sum of weights:", round(sum(w_gmv_ff3), 4), "\n")
## Sum of weights: 1
# ---- Realized portfolio return in 2015/02 ----
realized_ff3 <- sum(w_gmv_ff3 * ret_feb2015)

cat("\nRealized Portfolio Return (FF3 GMV) in 2015/02:",
    round(realized_ff3 * 100, 4), "%\n")
## 
## Realized Portfolio Return (FF3 GMV) in 2015/02: -1.3194 %
# Compare
cat("\n--- Comparison ---\n")
## 
## --- Comparison ---
cat("CAPM GMV Return 2015/02:", round(realized_capm * 100, 4), "%\n")
## CAPM GMV Return 2015/02: -1.2232 %
cat("FF3  GMV Return 2015/02:", round(realized_ff3  * 100, 4), "%\n")
## FF3  GMV Return 2015/02: -1.3194 %

2.8 Q8: Rolling-Window Backtest — CAPM vs FF3 GMV Portfolios (2015/02 to 2026/05)

# ---- Rolling-window backtest (60-month window) ----

# Get all months with full 60-month training windows
all_months <- merged_tbl$date
backtest_start <- as.Date("2015-02-01")
backtest_end   <- as.Date("2026-05-31")

backtest_months <- all_months[all_months >= backtest_start &
                              all_months <= backtest_end]

# Storage
results <- tibble(
  date         = as.Date(character()),
  ret_capm_gmv = numeric(),
  ret_ff3_gmv  = numeric()
)

for (t_date in backtest_months) {
  t_date <- as.Date(t_date)
  
  # Training window: 60 months ending one month before investment
  t_end_idx   <- which(all_months == t_date) - 1
  t_start_idx <- t_end_idx - 59
  
  if (t_start_idx < 1) next
  
  train <- merged_tbl[t_start_idx:t_end_idx, ]
  
  if (nrow(train) < 60) next
  
  # Prepare matrices
  ret_mat_t   <- as.matrix(train[, etf_cols])
  rf_t        <- train$RF
  exc_mat_t   <- sweep(ret_mat_t, 1, rf_t, "-")
  mkt_exc_t   <- train$Mkt.RF
  ff_mat_t    <- as.matrix(train[, c("Mkt.RF", "SMB", "HML")])
  
  # Compute weights
  tryCatch({
    S_capm   <- capm_cov_matrix(exc_mat_t, mkt_exc_t)
    w_capm_t <- gmv_weights(S_capm)
    
    S_ff3    <- ff3_cov_matrix(exc_mat_t, ff_mat_t)
    w_ff3_t  <- gmv_weights(S_ff3)
    
    # Realized return in month t
    ret_t <- as.numeric(merged_tbl[which(all_months == t_date), etf_cols])
    
    r_capm <- sum(w_capm_t * ret_t)
    r_ff3  <- sum(w_ff3_t  * ret_t)
    
    results <- bind_rows(results,
                         tibble(date = t_date,
                                ret_capm_gmv = r_capm,
                                ret_ff3_gmv  = r_ff3))
  }, error = function(e) NULL)
}

cat("Backtest complete. Number of monthly observations:", nrow(results), "\n")
## Backtest complete. Number of monthly observations: 135
print(head(results, 6))
## # A tibble: 6 × 3
##   date       ret_capm_gmv ret_ff3_gmv
##   <date>            <dbl>       <dbl>
## 1 2015-02-01     -0.0122     -0.0132 
## 2 2015-03-01      0.00289     0.00491
## 3 2015-04-01     -0.0183     -0.0209 
## 4 2015-05-01     -0.00372    -0.00441
## 5 2015-06-01     -0.0281     -0.0271 
## 6 2015-07-01      0.0315      0.0294
library(dplyr)
library(tidyr)
library(ggplot2)
library(scales)

# ---- Compute and plot cumulative returns ----
cum_results <- results %>%
  arrange(date) %>%
  mutate(
    cum_capm = cumprod(1 + ret_capm_gmv) - 1,
    cum_ff3  = cumprod(1 + ret_ff3_gmv)  - 1
  ) %>%
  select(date, cum_capm, cum_ff3) %>%
  pivot_longer(cols = c(cum_capm, cum_ff3),
               names_to  = "Strategy",
               values_to = "Cumulative_Return") %>%
  mutate(Strategy = recode(Strategy,
                           cum_capm = "CAPM-GMV",
                           cum_ff3  = "FF3-GMV"))

# Plot
ggplot(cum_results, aes(x = date, y = Cumulative_Return * 100, color = Strategy)) +
  geom_line(size = 1.1) +
  scale_color_manual(values = c("CAPM-GMV" = "#2C6FAC", "FF3-GMV" = "#E07B39")) +
  scale_y_continuous(labels = scales::percent_format(scale = 1)) +
  labs(
    title    = "Cumulative Returns: CAPM-GMV vs FF3-GMV Portfolio",
    subtitle = "Rolling 60-Month Window | 2015/02 – 2026/05",
    x        = "Date",
    y        = "Cumulative Return (%)",
    color    = "Strategy",
    caption  = "Note: Long-only GMV portfolio with 60-month rolling estimation window"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title    = element_text(face = "bold", size = 15),
    plot.subtitle = element_text(color = "gray40"),
    legend.position = "bottom",
    panel.grid.minor = element_blank()
  )

library(dplyr)
library(tibble)

# ---- Performance Statistics ----
perf_summary <- results %>%
  summarise(
    # CAPM GMV
    CAPM_Ann_Return = mean(ret_capm_gmv) * 12 * 100,
    CAPM_Ann_Stdev  = sd(ret_capm_gmv) * sqrt(12) * 100,
    CAPM_Sharpe     = (mean(ret_capm_gmv) / sd(ret_capm_gmv)) * sqrt(12),
    CAPM_Total_Ret  = (prod(1 + ret_capm_gmv) - 1) * 100,
    # FF3 GMV
    FF3_Ann_Return  = mean(ret_ff3_gmv) * 12 * 100,
    FF3_Ann_Stdev   = sd(ret_ff3_gmv) * sqrt(12) * 100,
    FF3_Sharpe      = (mean(ret_ff3_gmv) / sd(ret_ff3_gmv)) * sqrt(12),
    FF3_Total_Ret   = (prod(1 + ret_ff3_gmv) - 1) * 100
  )

# Format nicely
perf_tbl <- tibble(
  Metric            = c("Annualized Return (%)", "Annualized Std Dev (%)",
                        "Sharpe Ratio (annualized)", "Total Cumulative Return (%)"),
  `CAPM-GMV`        = c(round(perf_summary$CAPM_Ann_Return, 2),
                        round(perf_summary$CAPM_Ann_Stdev, 2),
                        round(perf_summary$CAPM_Sharpe, 3),
                        round(perf_summary$CAPM_Total_Ret, 2)),
  `FF3-GMV`         = c(round(perf_summary$FF3_Ann_Return, 2),
                        round(perf_summary$FF3_Ann_Stdev, 2),
                        round(perf_summary$FF3_Sharpe, 3),
                        round(perf_summary$FF3_Total_Ret, 2))
)

knitr::kable(perf_tbl,
             caption = "Performance Summary: CAPM-GMV vs FF3-GMV (2015/02 – 2026/05)",
             align   = c("l", "r", "r"))
Performance Summary: CAPM-GMV vs FF3-GMV (2015/02 – 2026/05)
Metric CAPM-GMV FF3-GMV
Annualized Return (%) 5.870 5.68
Annualized Std Dev (%) 10.060 10.15
Sharpe Ratio (annualized) 0.583 0.56
Total Cumulative Return (%) 82.620 78.70

3 Summary

This exam covered two major domains:

Textbook Theory (Ch. 7–10):

  • Ch. 7 — Diversification benefits diminish with fewer holdings; correlation structure determines portfolio risk; only Portfolio W cannot lie on the Markowitz efficient frontier; the B+C portfolio (ρ=0.10) is far superior to A+B (ρ=0.90) in risk reduction.
  • Ch. 8 — Beta measures only systematic risk; alpha captures abnormal return; R² indicates the proportion of variance explained by the market factor; only systematic risk is priced in a diversified portfolio.
  • Ch. 9 — Portfolio R lies above the SML (positive CAPM alpha) but below the CML (not fully efficient); CAPM predicts equal returns for equal-beta portfolios regardless of idiosyncratic risk.
  • Ch. 10 — APT equilibrium pricing via multi-factor models; no arbitrage opportunity for Large Cap Fund; GDP Fund utility weight is −2.2; both Stiles and McCracken are correct about the GDP Fund’s use cases.

Empirical R Analysis (Q1–Q8):

  • ETF data (2010–present) was downloaded and transformed into weekly/monthly simple returns.
  • Fama-French 3-factor data was merged with ETF returns for structural model estimation.
  • CAPM and FF3 factor models were used to estimate covariance matrices under structured assumptions, enabling cleaner estimates with fewer parameters than the historical sample covariance.
  • A rolling 60-month window backtest generated monthly GMV portfolio returns from 2015/02–2026/05, showing the relative performance of the two covariance-estimation approaches.

Final Exam — Portfolio Analysis | June 2026