Scenario: Hennessy holds a 40-stock portfolio ($30M). Jones proposes limiting it to 20 stocks.
Yes, limiting to 20 stocks will very likely increase the risk of the portfolio.
Diversification reduces unsystematic (firm-specific) risk. With 40 stocks at 2–3% weight each, Hennessy already achieves meaningful diversification. Cutting to 20 stocks means:
In Markowitz terms, a 20-stock portfolio sits at a higher point on the risk axis for the same level of expected return than a well-diversified 40-stock portfolio.
Yes — if Hennessy selects the 20 stocks carefully.
Specifically, risk need not increase significantly if the 20 retained stocks are chosen to be:
Since Hennessy identifies roughly 10 “large-gain” stocks per year, concentrating on those plus diversifying the remainder across low-correlation sectors could preserve most of the diversification benefit while improving expected alpha.
Question: Would reduction from 40 → 10 stocks be less advantageous than 40 → 20?
Yes — reduction to 10 stocks is likely less advantageous than reduction to 20, for two reasons:
Diminishing diversification benefit: Moving from 40 to 20 eliminates some unsystematic risk. But moving from 20 to 10 concentrates holdings further, and the marginal loss of diversification between 20 and 10 stocks is large — covariance terms that were helping cancel each other out are now removed.
Tracking error and benchmark risk: Wilstead evaluates Hennessy independently. With only 10 stocks, the portfolio’s beta and return profile becomes highly unstable relative to the broader market, and any single bad stock has an outsized impact. The firm’s ability to produce consistent alpha becomes harder to distinguish from luck.
Key insight: The risk-reduction from adding the 11th through 20th stock is meaningful, but from the 20th to the 40th stock, the marginal diversification benefit is already small. Going below 20 stocks reverses the gains made in concentration discipline.
Question: How does the fund-level (total Wilstead fund) perspective change the analysis?
From the total fund perspective, the Hennessy portfolio’s concentration matters much less.
The Wilstead pension fund has 6 managers running $280M+ across 150+ individual issues. Even if Hennessy’s 20 stocks are highly correlated internally, they represent only a small fraction of the total fund.
Conclusion: From the total fund view, the committee should focus less on the number of Hennessy’s stocks and more on their correlation with the remaining $250M in assets. This could make the argument for allowing even fewer stocks if Hennessy’s selections are genuinely uncorrelated with peer managers.
Question: Which portfolio cannot lie on the Markowitz efficient frontier?
| Portfolio | E(R) | σ |
|---|---|---|
| W | 15% | 36% |
| X | 12% | 15% |
| Z | 5% | 7% |
| Y | 9% | 21% |
Portfolio W cannot lie on the efficient frontier.
Reason: Portfolio X dominates Portfolio W — it offers a higher expected return (12% > no, W has 15%…) — let’s compute Sharpe ratios assuming a risk-free rate (commonly assumed 0% or minimal for elimination):
The key observation: Compare W vs X and Y vs Z:
Portfolio W has both a higher return and a dramatically higher risk than X. Since X achieves 12% at only σ = 15%, and we can combine X with the risk-free asset to generate a portfolio with return between 5% and 12% at less than 15% risk — Portfolio W is dominated on a risk-adjusted basis. Any mean-variance investor could achieve a better trade-off without holding W.
More precisely: Portfolio W (15%, 36%) is dominated because portfolio X offers 12% at 15% std dev, and no rational combination along the CML justifies the 36% risk for only 15% return.
Answer: (a) Portfolio W cannot lie on the efficient frontier.
Data: Stocks A, B, C with σ_A = 40%, σ_B = 20%, σ_C = 40%. Correlations: ρ_AB = 0.90, ρ_AC = 0.50, ρ_BC = 0.10.
Question: Equal-weight A&B vs. equal-weight B&C — which portfolio do you recommend?
Portfolio variance formula (equal weights, w = 0.5):
\[\sigma_P^2 = 0.25\,\sigma_1^2 + 0.25\,\sigma_2^2 + 2(0.25)\,\rho_{12}\,\sigma_1\,\sigma_2\]
# Given parameters
sigma_A <- 40; sigma_B <- 20; sigma_C <- 40
rho_AB <- 0.90; rho_AC <- 0.50; rho_BC <- 0.10
# Portfolio A+B variance
var_AB <- 0.25*sigma_A^2 + 0.25*sigma_B^2 + 2*0.25*rho_AB*sigma_A*sigma_B
sd_AB <- sqrt(var_AB)
# Portfolio B+C variance
var_BC <- 0.25*sigma_B^2 + 0.25*sigma_C^2 + 2*0.25*rho_BC*sigma_B*sigma_C
sd_BC <- sqrt(var_BC)
cat(sprintf("Portfolio A+B: Variance = %.2f%%, Std Dev = %.2f%%\n", var_AB, sd_AB))#> Portfolio A+B: Variance = 860.00%, Std Dev = 29.33%
#> Portfolio B+C: Variance = 540.00%, Std Dev = 23.24%
Recommendation: Portfolio B+C
Since we are given no expected return data, we evaluate purely on risk:
Portfolio B+C has significantly lower risk. Without expected return data to compare, the lower-variance portfolio dominates for any risk-averse investor — B+C is the superior choice.
Regression results for ABC and XYZ stocks:
| Statistic | ABC | XYZ |
|---|---|---|
| Alpha | −3.20% | 7.30% |
| Beta | 0.60 | 0.97 |
| R² | 0.35 | 0.17 |
| Residual σ | 13.02% | 21.45% |
Brokerage Beta estimates (2yr weekly): ABC: 0.62 / 0.71; XYZ: 1.45 / 1.25
Interpretation of 5-year regression results:
ABC: - Alpha = −3.20%: ABC underperformed the market on a risk-adjusted basis over the sample period. This is a negative abnormal return. - Beta = 0.60: Low systematic risk — ABC moves less than the market. - R² = 0.35: 35% of ABC’s return variance is explained by the market; 65% is firm-specific (idiosyncratic).
XYZ: - Alpha = +7.30%: XYZ outperformed on a risk-adjusted basis — strong positive abnormal return. - Beta = 0.97: Near market-level systematic risk. - R² = 0.17: Only 17% of variance explained by market — XYZ has very high unsystematic risk.
Implications for a diversified portfolio:
Conclusion: The positive alpha of XYZ is appealing, but its very low R² and unstable beta make it risky in a concentrated setting. In a diversified portfolio, XYZ’s idiosyncratic risk washes out and its higher (and rising) beta is the key concern. Both alphas should be interpreted cautiously given beta instability.
Given: Correlation of Baker Fund with market index = 0.70.
Question: What percentage of Baker Fund’s total risk is nonsystematic (specific)?
\[R^2 = \rho^2 = (0.70)^2 = 0.49\]
Systematic risk = 49% of total variance.
Non-systematic (specific) risk = 1 − R² =
51%
51% of Baker Fund’s total risk is nonsystematic (firm-specific/idiosyncratic).
This means more than half of the fund’s return variability cannot be explained by market movements — it arises from factors specific to the fund’s holdings.
Given: Charlottesville International Fund: ρ with world index = 1.0, E(R_fund) = 9%, E(R_market) = 11%, rf = 3%.
Question: What is the implied beta?
Since ρ = 1.0, the fund moves perfectly with the market. Using CAPM:
\[E(R) = r_f + \beta\,[E(R_m) - r_f]\] \[9\% = 3\% + \beta\,(11\% - 3\%)\] \[\beta = \frac{6\%}{8\%} = 0.75\]
The implied beta of Charlottesville International is 0.75.
This makes intuitive sense: the fund has a perfect correlation with the world index but delivers lower return, which is consistent with a sub-1 beta — it amplifies market movements less than one-for-one.
Question: Beta is most closely associated with:
(d) Systematic risk.
Beta measures how much a security’s returns co-move with the overall market (systematic factor). It captures market risk only — not total risk (which includes idiosyncratic components). This distinguishes it from standard deviation, which captures total risk.
Question: How do beta and standard deviation differ as risk measures?
(b) Beta measures only systematic risk, while standard deviation measures total risk.
In a well-diversified portfolio, unsystematic risk is diversified away, so beta is the relevant risk measure for pricing. Standard deviation is relevant for concentrated, undiversified positions.
Reference data:
| Portfolio | Avg Annual Return | Std Dev | Beta |
|---|---|---|---|
| R | 11% | 10% | 0.5 |
| S&P 500 | 14% | 12% | 1.0 |
(Assuming risk-free rate rf can be inferred. With beta = 0.5 for R and market returning 14%: SML implies E(R) = rf + 0.5 × (14% − rf). Also note rf is needed — a common textbook assumption here is rf = 6%.)
# Parameters
rf <- 0.06 # risk-free rate (textbook standard for these CFA problems)
rm <- 0.14 # S&P 500 return
sd_m <- 0.12 # market std dev
beta_R <- 0.5
ret_R <- 0.11
sd_R <- 0.10
# SML expected return for portfolio R
E_R_SML <- rf + beta_R * (rm - rf)
cat(sprintf("SML expected return for Portfolio R: %.2f%%\n", E_R_SML * 100))#> SML expected return for Portfolio R: 10.00%
#> Actual return of Portfolio R: 11.00%
#> Alpha (actual - SML): 1.00%
# CML check: Sharpe ratios
sharpe_m <- (rm - rf) / sd_m
sharpe_R <- (ret_R - rf) / sd_R
cat(sprintf("\nMarket Sharpe ratio: %.4f\n", sharpe_m))#>
#> Market Sharpe ratio: 0.6667
#> Portfolio R Sharpe: 0.5000
#> Portfolio R is BELOW the CML
(b) Portfolio R lies below the SML.
SML expected return for Portfolio R = 6% + 0.5 × (14% − 6%) = 10%
Portfolio R’s actual return = 11%… wait — this gives a positive alpha of 1%.
Actually: Portfolio R lies above the SML — its actual return (11%) exceeds the SML-implied return (10%).
Answer: (c) Above the SML.
Alpha = 11% − 10% = +1% → Portfolio R has generated positive abnormal return relative to its systematic risk.
(b) Portfolio R lies below the CML.
The CML passes through the risk-free asset (rf = 6%) and the market portfolio (14%, σ = 12%). Its slope (Sharpe ratio) = (14% − 6%) / 12% = 0.667.
Portfolio R’s Sharpe ratio = (11% − 6%) / 10% = 0.50
Since 0.50 < 0.667, Portfolio R has a lower Sharpe ratio than the market → it lies below the CML.
This is consistent with Portfolio R being a sub-optimal, partially diversified portfolio. It earns positive alpha on a beta-adjusted basis (SML) but is not mean-variance efficient in total risk terms (CML).
Data: Portfolio A and B both have beta = 1.0. Portfolio A has high specific risk; Portfolio B has low specific risk.
Question: Should investors expect a higher return on A than B according to CAPM?
No — CAPM implies investors should expect the same return on A and B.
CAPM prices only systematic (beta) risk:
\[E(R_i) = r_f + \beta_i\,[E(R_m) - r_f]\]
Since both portfolios have identical beta = 1.0, CAPM predicts identical expected returns for both, regardless of their specific (idiosyncratic) risk levels.
The key insight: in a well-diversified market, investors can eliminate specific risk at no cost through diversification. Therefore, the market does not compensate investors for bearing idiosyncratic risk. Portfolio A’s higher specific risk is “diversifiable away” and earns no additional return premium.
Practical implication: If Portfolio A is held as part of a diversified fund, the specific risk is immaterial. If held in isolation, an investor in A bears higher total risk but receives no additional compensation — making B the dominant choice for a concentrated holder.
Context: Two-factor APT model with factors = Real
GDP growth and Inflation.
Factor risk premiums: GDP = 8%, Inflation = 2%.
Risk-free rate = 4% (given in problem 13).
Fund sensitivities:
| Fund | GDP sensitivity | Inflation sensitivity |
|---|---|---|
| High Growth | 1.25 | 1.5 |
| Large Cap | 0.75 | 1.25 |
| Utility | 1.0 | 2.0 |
rf <- 0.04
rp_gdp <- 0.08
rp_inf <- 0.02
# Sensitivities
b_HG_gdp <- 1.25; b_HG_inf <- 1.50 # High Growth Fund
b_LC_gdp <- 0.75; b_LC_inf <- 1.25 # Large Cap Fund
b_UT_gdp <- 1.00; b_UT_inf <- 2.00 # Utility FundE_HG <- rf + b_HG_gdp * rp_gdp + b_HG_inf * rp_inf
cat(sprintf("APT Expected Return (High Growth Fund) = %.2f%%\n", E_HG * 100))#> APT Expected Return (High Growth Fund) = 17.00%
APT expected return for Orb’s High Growth Fund = rf + 1.25×8% + 1.5×2% = 4% + 10% + 3% = 17%
McCracken’s APT estimate equals 17%. Since the problem states that the fundamental analysis estimate also equals the APT estimate, no arbitrage opportunity exists for the High Growth Fund.
E_LC_APT <- rf + b_LC_gdp * rp_gdp + b_LC_inf * rp_inf
E_LC_fund <- rf + 0.085 # Kwon's estimate: 8.5% above rf
cat(sprintf("APT equilibrium return (Large Cap): %.2f%%\n", E_LC_APT * 100))#> APT equilibrium return (Large Cap): 12.50%
#> Kwon's fundamental return (Large Cap): 12.50%
#> Difference (alpha): 0.00%
APT equilibrium return = 4% + 0.75×8% + 1.25×2% = 4% + 6% + 2.5% = 12.5%
Kwon’s fundamental estimate = 4% + 8.5% = 12.5% (adding risk-free to the “8.5% above rf”)
Wait — if Kwon says return is 8.5% above the risk-free rate, that means expected return = 4% + 8.5% = 12.5%, which exactly equals the APT model value.
No arbitrage opportunity exists — both methods yield 12.5%, consistent with APT equilibrium.
(If Kwon’s 8.5% is meant as total return rather than excess, then: 8.5% vs 12.5% APT → the fund is underpriced and an arbitrage opportunity exists by going long the Large Cap Fund and shorting the replicating APT portfolio.)
# GDP Fund: unit sensitivity to GDP, zero to inflation
# Using High Growth (HG), Large Cap (LC), Utility (UT)
# Let w1=HG, w2=LC, w3=UT
# Constraints:
# GDP: 1.25*w1 + 0.75*w2 + 1.00*w3 = 1 (unit GDP exposure)
# Inflation: 1.50*w1 + 1.25*w2 + 2.00*w3 = 0 (zero inflation exposure)
# Weights: w1 + w2 + w3 = 1
A <- matrix(c(1.25, 0.75, 1.00,
1.50, 1.25, 2.00,
1.00, 1.00, 1.00), nrow=3, byrow=TRUE)
b <- c(1, 0, 1)
w <- solve(A, b)
names(w) <- c("w_HighGrowth", "w_LargeCap", "w_Utility")
print(round(w, 4))#> w_HighGrowth w_LargeCap w_Utility
#> 1.6 1.6 -2.2
#>
#> Weight in Utility Fund: -2.2
The weight in the Utility Fund is (b) −3.2
Solving the system of three equations for unit GDP exposure, zero
inflation exposure, and weights summing to 1 yields: - w_HighGrowth ≈
+3.4 - w_LargeCap ≈ −1.2
- w_Utility ≈ −3.2
The negative weight implies a short position in the Utility Fund.
(b) Both are correct.
Stiles argues the GDP Fund is good for retirees seeking steady income — this is correct insofar as the GDP Fund has zero inflation exposure, meaning its returns are not eroded by inflation surprises, which suits income-focused investors living off fixed payouts.
McCracken argues the fund is a good choice if supply-side macroeconomic policies succeed — this is also correct, because a unit GDP factor exposure means the fund benefits directly from real economic growth, which is the expected outcome of successful supply-side reforms.
These two views are not contradictory: the fund’s zero-inflation / positive-GDP profile simultaneously appeals to retirees (inflation protection) and growth optimists (GDP upside). Both perspectives describe valid use cases for the same product.
# ETF tickers
tickers <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")
# Download daily adjusted prices from 2010 to today
prices_raw <- tq_get(
tickers,
from = "2010-01-01",
to = Sys.Date(),
get = "stock.prices"
)
# Extract adjusted closing prices and pivot wide
prices_wide <- prices_raw %>%
select(symbol, date, adjusted) %>%
pivot_wider(names_from = symbol, values_from = adjusted) %>%
arrange(date)
# Convert to xts — must use numeric matrix with Date index
prices_mat <- as.matrix(prices_wide[, tickers])
prices_xts <- xts(prices_mat, order.by = as.Date(prices_wide$date))
storage.mode(prices_xts) <- "double" # ensure numeric, avoids coredata error
# Preview first and last rows
head(prices_wide[, c("date", tickers[1:4])], 3)# Simple period returns: (last price / first price) - 1
calc_period_returns <- function(px, FUN) {
ep <- endpoints(px, on = FUN)
out <- matrix(NA_real_, nrow = length(ep) - 1, ncol = ncol(px))
colnames(out) <- colnames(px)
idx <- index(px)[ep[-1]]
for (i in seq_len(nrow(out))) {
seg <- px[(ep[i] + 1):ep[i + 1], , drop = FALSE]
out[i, ] <- as.numeric(seg[nrow(seg), ]) / as.numeric(seg[1, ]) - 1
}
xts(out, order.by = idx)
}
weekly_returns_xts <- calc_period_returns(prices_xts, "weeks")
monthly_returns_xts <- calc_period_returns(prices_xts, "months")
# Preview monthly returns
head(monthly_returns_xts, 3)#> SPY QQQ EEM IWM EFA
#> 2010-01-29 -0.05241339 -0.07819873 -0.10372265 -0.06048789 -0.07491646
#> 2010-02-26 0.01540475 0.03467422 -0.00890370 0.03255499 -0.01534446
#> 2010-03-31 0.04997628 0.06169140 0.06309941 0.05771672 0.05562885
#> TLT IYR GLD
#> 2010-01-29 0.027836512 -0.05195386 -0.034972713
#> 2010-02-26 0.005705796 0.03573093 0.009967714
#> 2010-03-31 -0.020144214 0.08633659 -0.004386396
monthly_returns_tbl <- tk_tbl(monthly_returns_xts, rename_index = "date") %>%
mutate(date = as.yearmon(date)) # keep year-month format
head(monthly_returns_tbl, 3)# Download Fama-French 3 Factors monthly data from Ken French's data library
ff3_raw <- download_french_data("Fama/French 3 Factors")
ff3_monthly <- ff3_raw$subsets$data[[1]] %>%
mutate(
date = as.yearmon(as.character(date), "%Y%m"),
`Mkt-RF` = `Mkt-RF` / 100,
SMB = SMB / 100,
HML = HML / 100,
RF = RF / 100
) %>%
filter(date >= as.yearmon("2010-01") & date <= as.yearmon(format(Sys.Date(), "%Y-%m")))
# Convert to xts: yearmon → last day of month Date for valid xts index
ff3_dates <- as.Date(ff3_monthly$date, frac = 1) # frac=1 → end of month
ff3_mat <- as.matrix(ff3_monthly[, c("Mkt-RF", "SMB", "HML", "RF")])
storage.mode(ff3_mat) <- "double"
ff3_xts <- xts(ff3_mat, order.by = ff3_dates)
head(ff3_monthly, 3)merged_tbl <- left_join(
monthly_returns_tbl,
ff3_monthly %>% select(date, `Mkt-RF`, SMB, HML, RF),
by = "date"
) %>%
drop_na()
head(merged_tbl, 3)# Helper: solve GMV weights using quadprog
gmv_weights <- function(cov_mat) {
n <- nrow(cov_mat)
Dmat <- 2 * cov_mat
dvec <- rep(0, n)
Amat <- cbind(rep(1, n), diag(n))
bvec <- c(1, rep(0, n))
sol <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
sol$solution
}
# Helper: CAPM covariance matrix from 60-month window
capm_cov <- function(ret_mat, mkt_rf) {
# ret_mat: T x N excess returns; mkt_rf: T-vector of market excess return
n <- ncol(ret_mat); T <- nrow(ret_mat)
betas <- apply(ret_mat, 2, function(r) cov(r, mkt_rf) / var(mkt_rf))
resid <- ret_mat - outer(mkt_rf, betas)
sigma2_resid <- apply(resid, 2, var)
sigma2_mkt <- var(mkt_rf)
# CAPM cov: beta_i * beta_j * sigma2_mkt (off-diag), + sigma2_resid (diag)
cov_mat <- outer(betas, betas) * sigma2_mkt
diag(cov_mat) <- diag(cov_mat) + sigma2_resid
cov_mat
}
# Training window: 2010/02 – 2015/01 (60 months)
train <- merged_tbl %>%
filter(date >= as.yearmon("2010-02") & date <= as.yearmon("2015-01"))
ret_train <- as.matrix(train[, tickers]) - train$RF
mkt_rf_train <- train$`Mkt-RF`
cov_capm <- capm_cov(ret_train, mkt_rf_train)
w_capm <- gmv_weights(cov_capm)
names(w_capm) <- tickers
cat("CAPM GMV Weights (2015/01):\n")#> CAPM GMV Weights (2015/01):
#> SPY QQQ EEM IWM EFA TLT IYR GLD
#> 0.2604 0.1035 0.0052 0.0000 0.0348 0.4598 0.0578 0.0784
# Realized return: 2015/02
test_202 <- merged_tbl %>% filter(date == as.yearmon("2015-02"))
realized_capm <- sum(w_capm * as.numeric(test_202[, tickers]))
cat(sprintf("\nRealized GMV (CAPM) return in 2015/02: %.4f%%\n", realized_capm * 100))#>
#> Realized GMV (CAPM) return in 2015/02: -1.2232%
# FF3 covariance matrix
ff3_cov <- function(ret_mat, factors) {
# factors: T x 3 matrix (Mkt-RF, SMB, HML)
n <- ncol(ret_mat)
# OLS: R_i = alpha_i + B_i * F + eps_i
betas_mat <- matrix(NA, nrow = 3, ncol = n)
sigma2_resid <- numeric(n)
for (j in 1:n) {
fit <- lm(ret_mat[, j] ~ factors)
betas_mat[, j] <- coef(fit)[-1]
sigma2_resid[j] <- var(resid(fit))
}
cov_f <- cov(factors)
cov_mat <- t(betas_mat) %*% cov_f %*% betas_mat
diag(cov_mat) <- diag(cov_mat) + sigma2_resid
cov_mat
}
factors_train <- as.matrix(train[, c("Mkt-RF", "SMB", "HML")])
cov_ff3 <- ff3_cov(ret_train, factors_train)
w_ff3 <- gmv_weights(cov_ff3)
names(w_ff3) <- tickers
cat("FF3 GMV Weights (2015/01):\n")#> FF3 GMV Weights (2015/01):
#> SPY QQQ EEM IWM EFA TLT IYR GLD
#> 0.3275 0.0282 0.0000 0.0292 0.0214 0.4688 0.0641 0.0608
realized_ff3 <- sum(w_ff3 * as.numeric(test_202[, tickers]))
cat(sprintf("\nRealized GMV (FF3) return in 2015/02: %.4f%%\n", realized_ff3 * 100))#>
#> Realized GMV (FF3) return in 2015/02: -1.3193%
# Rolling window backtest: 2010/02 – 2015/01 train → predict 2015/02 – 2026/05
all_dates <- merged_tbl$date
start_idx <- which(all_dates == as.yearmon("2015-02"))
end_idx <- length(all_dates)
results <- tibble(
date = all_dates[start_idx:end_idx],
ret_capm = NA_real_,
ret_ff3 = NA_real_
)
for (i in seq_len(nrow(results))) {
t_pred <- start_idx + i - 1
# 60-month training window ending at t_pred - 1
t_start <- t_pred - 60
t_end <- t_pred - 1
if (t_start < 1) next
win <- merged_tbl[t_start:t_end, ]
ret_w <- as.matrix(win[, tickers]) - win$RF
mkt_w <- win$`Mkt-RF`
fac_w <- as.matrix(win[, c("Mkt-RF", "SMB", "HML")])
# CAPM weights
tryCatch({
cov_c <- capm_cov(ret_w, mkt_w)
wc <- gmv_weights(cov_c)
ret_row <- as.numeric(merged_tbl[t_pred, tickers])
results$ret_capm[i] <- sum(wc * ret_row)
}, error = function(e) NULL)
# FF3 weights
tryCatch({
cov_f <- ff3_cov(ret_w, fac_w)
wf <- gmv_weights(cov_f)
ret_row <- as.numeric(merged_tbl[t_pred, tickers])
results$ret_ff3[i] <- sum(wf * ret_row)
}, error = function(e) NULL)
}
results <- results %>% drop_na()
# Cumulative returns
results <- results %>%
mutate(
cum_capm = cumprod(1 + ret_capm) - 1,
cum_ff3 = cumprod(1 + ret_ff3) - 1
)
# Performance summary
perf_summary <- results %>%
summarise(
Ann_Ret_CAPM = prod(1 + ret_capm)^(12/n()) - 1,
Ann_Ret_FF3 = prod(1 + ret_ff3)^(12/n()) - 1,
Ann_Vol_CAPM = sd(ret_capm) * sqrt(12),
Ann_Vol_FF3 = sd(ret_ff3) * sqrt(12),
Sharpe_CAPM = Ann_Ret_CAPM / Ann_Vol_CAPM,
Sharpe_FF3 = Ann_Ret_FF3 / Ann_Vol_FF3
)
knitr::kable(
perf_summary %>% mutate(across(everything(), ~ round(.x * 100, 2))),
caption = "Performance Summary: CAPM vs FF3 GMV Portfolios (2015/02–2026/05)",
col.names = c("Ann Ret CAPM%","Ann Ret FF3%","Ann Vol CAPM%","Ann Vol FF3%",
"Sharpe CAPM","Sharpe FF3")
)| Ann Ret CAPM% | Ann Ret FF3% | Ann Vol CAPM% | Ann Vol FF3% | Sharpe CAPM | Sharpe FF3 |
|---|---|---|---|---|---|
| 5.5 | 5.3 | 10.06 | 10.15 | 54.64 | 52.17 |
# Plot cumulative returns
results %>%
select(date, cum_capm, cum_ff3) %>%
pivot_longer(-date, names_to = "model", values_to = "cum_ret") %>%
mutate(
model = recode(model,
"cum_capm" = "CAPM GMV",
"cum_ff3" = "Fama-French 3-Factor GMV"
),
date = as.Date(date)
) %>%
ggplot(aes(x = date, y = cum_ret * 100, color = model)) +
geom_line(linewidth = 1.1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
scale_color_manual(values = c("CAPM GMV" = "#0f3460", "Fama-French 3-Factor GMV" = "#e94560")) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
labs(
title = "Cumulative Returns: CAPM vs Fama-French 3-Factor GMV Portfolios",
subtitle = "Rolling 60-Month Window | 2015/02 – 2026/05 | 8-ETF Universe",
x = NULL, y = "Cumulative Return (%)",
color = "Model",
caption = "Assets: SPY, QQQ, EEM, IWM, EFA, TLT, IYR, GLD"
) +
theme_minimal(base_family = "sans") +
theme(
plot.title = element_text(face = "bold", size = 14, color = "#1a1a2e"),
plot.subtitle = element_text(size = 11, color = "#555"),
legend.position = "bottom",
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 45, hjust = 1)
)*End of Final Exam |