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.
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.
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.)
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:
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):
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.
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.
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 |
| R² | 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).
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%.
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.
Q: The concept of beta is most closely associated with:
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.
Q: Beta and standard deviation differ as risk measures in that beta measures:
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.
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
Q: When plotting portfolio R relative to the SML, portfolio R lies:
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.
Q: When plotting portfolio R relative to the CML, portfolio R lies:
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.
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.
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)
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%.
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.
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.
Q: With respect to Stiles’s and McCracken’s comments about the GDP Fund:
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.
# 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
## 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
# 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):
## 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
##
## Monthly Returns (first 5 rows):
## 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
# 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:
## # 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
##
## Dimensions: 198 rows x 9 cols
# 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):
## # 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
##
## Rows available: 196
# 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):
## # 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>
##
## Dimensions: 196 rows x 13 cols
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):
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.2604 0.1035 0.0052 0.0000 0.0348 0.4598 0.0578 0.0784
## 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 %
# ---- 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):
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.3275 0.0282 0.0000 0.0292 0.0214 0.4689 0.0641 0.0608
## 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 %
##
## --- Comparison ---
## CAPM GMV Return 2015/02: -1.2232 %
## FF3 GMV Return 2015/02: -1.3194 %
# ---- 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
## # 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"))| 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 |
This exam covered two major domains:
Textbook Theory (Ch. 7–10):
Empirical R Analysis (Q1–Q8):
Final Exam — Portfolio Analysis | June 2026