Background (CFA 1–3): Hennessy & Associates manages a $30 million equity portfolio for the multimanager Wilstead Pension Fund. Hennessy is a bottom-up stock picker holding about 40 stocks (2%–3% of funds each). Jones proposes limiting the portfolio to no more than 20 stocks.
a. Will the limitation to 20 stocks likely increase or decrease the risk of the portfolio? Explain.
The restriction to 20 stocks will most likely increase the risk of the portfolio. Going from 40 to 20 issues reduces diversification, so the firm-specific (nonsystematic) component of risk rises. However, the increase should be relatively small, because most of the benefit of diversification is already achieved with roughly 20–25 well-chosen stocks: the marginal risk reduction from adding stocks beyond 20 is modest, so removing them adds back only a modest amount of risk.
b. Is there any way Hennessy could reduce the number of issues from 40 to 20 without significantly affecting risk? Explain.
Yes. Hennessy can make the reduction in a deliberately diversified way rather than simply doubling up on its favorite names. By spreading the 20 remaining stocks across different industries, sectors, and risk characteristics (i.e., choosing stocks whose returns have relatively low correlations with each other — for example through stratified sampling across sectors or a simple optimization), the portfolio keeps most of its diversification, and total risk will be only slightly higher than with 40 stocks.
Reduction from 40 to 20 stocks is less harmful than a further reduction from 20 to 10 because diversification benefits diminish at a decreasing rate — equivalently, concentration costs accelerate. The portfolio standard deviation as a function of the number of holdings is convex: dropping from 40 to 20 stocks adds only a small amount of nonsystematic risk, but dropping from 20 to 10 adds much more nonsystematic risk. Therefore, while the gain from concentrating money in Hennessy’s 10 very best ideas might add some extra expected return, that gain would have to overcome a far larger increase in risk than the move to 20 stocks did. Evaluated as a stand-alone portfolio (as Wilstead proposes to do), the reduction to 10 issues is therefore less likely to be advantageous than the reduction to 20.
The committee member’s point is that Hennessy’s portfolio should not be judged in isolation: it is only about $30 million out of an aggregate fund of roughly $280 million (the other five managers run $250 million spread over more than 150 issues). At the total fund level, Hennessy’s firm-specific risk is largely diversified away by the holdings of the other managers. Hence the increase in nonsystematic risk caused by concentrating Hennessy’s portfolio in 20 — or even 10 — stocks has only a negligible effect on the total fund’s risk. Taking this broader view, the committee can comfortably allow the more concentrated portfolio (and capture more of Hennessy’s stock-picking skill), since the relevant risk to the plan is the risk of the overall fund, not of each sleeve separately.
Which portfolio cannot lie on the efficient frontier?
| Portfolio | Expected Return (%) | Standard Deviation (%) |
|---|---|---|
| a. W | 15 | 36 |
| b. X | 12 | 15 |
| c. Z | 5 | 7 |
| d. Y | 9 | 21 |
Answer: (d) Portfolio Y. Portfolio Y is dominated by portfolio X, which offers a higher expected return (12% > 9%) with a lower standard deviation (15% < 21%). A dominated portfolio can never lie on the Markowitz efficient frontier. None of the other portfolios is dominated by another portfolio in the table.
Standard deviations: \(\sigma_A = 40\%\), \(\sigma_B = 20\%\), \(\sigma_C = 40\%\); correlations: \(\rho_{AB}=0.90\), \(\rho_{AC}=0.50\), \(\rho_{BC}=0.10\). Compare an equally weighted portfolio of A and B with an equally weighted portfolio of B and C:
\[\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\]
sd_AB <- sqrt(0.5^2*40^2 + 0.5^2*20^2 + 2*0.5*0.5*0.90*40*20)
sd_BC <- sqrt(0.5^2*20^2 + 0.5^2*40^2 + 2*0.5*0.5*0.10*20*40)
c(`SD of 50/50 A&B (%)` = sd_AB, `SD of 50/50 B&C (%)` = sd_BC)## SD of 50/50 A&B (%) SD of 50/50 B&C (%)
## 29.32576 23.23790
Recommendation: the portfolio of B and C. Its standard deviation (23.24%) is clearly lower than that of the A&B portfolio (29.33%). Even though B and C individually have the same volatilities as B and A (20% and 40%), the much lower correlation between B and C (0.10 versus 0.90) delivers a far greater diversification benefit. Since the tables give no information about expected returns, the lower-risk combination B&C is the rational choice.
Regression of annualized monthly excess returns on the market excess return:
| Statistic | ABC | XYZ |
|---|---|---|
| Alpha | −3.20% | 7.3% |
| Beta | 0.60 | 0.97 |
| \(R^2\) | 0.35 | 0.17 |
| Residual std. dev. | 13.02% | 21.45% |
What the results tell the analyst about the sample period:
Implications for the future:
If \(\rho\) between Baker Fund and the market is 0.70, the proportion of total risk that is systematic is \(\rho^2 = 0.70^2 = 0.49\). Therefore the fraction of total risk that is specific (nonsystematic) is:
\[1-\rho^2 = 1 - 0.49 = 0.51 = \textbf{51\%}\]
Correlation with the world index is 1.0, so the fund lies exactly on the regression line and CAPM/SML pricing applies:
\[E(r) = r_f + \beta\,[E(r_M) - r_f] \;\Rightarrow\; 9\% = 3\% + \beta\,(11\% - 3\%)\]
\[\beta = \frac{9\% - 3\%}{11\% - 3\%} = \frac{6}{8} = \textbf{0.75}\]
The concept of beta is most closely associated with:
Answer: (d) systematic risk. Beta measures the sensitivity of a security’s return to the market return, i.e., its systematic (market, non-diversifiable) risk.
Beta and standard deviation differ as risk measures in that beta measures:
Answer: (b) — beta measures only systematic risk, while standard deviation measures total risk (systematic + unsystematic).
Data for CFA 8 and 9:
| Portfolio | Avg. Annual Return | Standard Deviation | Beta |
|---|---|---|---|
| R | 11% | 10% | 0.5 |
| S&P 500 | 14% | 12% | 1.0 |
When plotting portfolio R relative to the SML, portfolio R lies:
Answer: (d) Insufficient data given — you need to know the risk-free rate.
The SML is the line from the risk-free rate through the market portfolio in \((\beta, E(r))\) space: \(E(r) = r_f + \beta\,[E(r_M) - r_f]\). The table gives the market’s return (14%) and portfolio R’s beta (0.5) and return (11%), but not the risk-free rate, so the SML cannot be drawn and R’s position relative to it cannot be determined. At \(\beta = 0.5\) the required return is \(7\% + 0.5\,r_f\): if \(r_f < 8\%\) portfolio R (11%) would plot above the SML, and if \(r_f > 8\%\) it would plot below — the answer depends on the missing risk-free rate.
When plotting portfolio R relative to the capital market line (CML), portfolio R lies:
Answer: (d) Insufficient data given — you need to know the risk-free rate.
The CML is the line from the risk-free rate through the market portfolio in \((\sigma, E(r))\) space: \(E(r) = r_f + \dfrac{E(r_M)-r_f}{\sigma_M}\,\sigma\). Without the risk-free rate the CML cannot be drawn, so R’s position relative to it cannot be plotted. (Note that for any plausible non-negative \(r_f\), the CML at \(\sigma = 10\%\) would give \(11.67\% + 0.167\,r_f > 11\%\), i.e., R would lie below the CML — it bears some nonsystematic risk, and only perfectly diversified portfolios reach the CML — but strictly the line itself requires \(r_f\) to be specified.)
| Portfolio A | Portfolio B | |
|---|---|---|
| Systematic risk (beta) | 1.0 | 1.0 |
| Specific risk for each individual security | High | Low |
No — under the CAPM investors should expect the same return on both portfolios. The CAPM prices only systematic risk: expected return depends solely on beta, \(E(r)=r_f+\beta[E(r_M)-r_f]\). Both portfolios have \(\beta = 1.0\), so both have an expected return equal to the market’s. The high specific (firm-level) risk of portfolio A is diversifiable — the market does not compensate investors for risk they can eliminate for free by diversifying — so it commands no additional risk premium.
Setup: Two-factor APT model with factor risk premiums: real GDP = 8%, inflation = 2%. Sensitivities (GDP, inflation): High Growth Fund (1.25, 1.5); Large Cap Fund (0.75, 1.25); Utility Fund (1.0, 2.0).
With \(r_f = 4\%\), the APT expected return of the High Growth Fund:
\[E(r) = 4\% + 1.25\times 8\% + 1.5\times 2\% = 4\% + 10\% + 3\% = \textbf{17\%}\]
No arbitrage opportunity is available. The APT estimate for the Large Cap Fund is
\[E(r) = r_f + 0.75\times 8\% + 1.25\times 2\% = r_f + 6\% + 2.5\% = r_f + 8.5\%,\]
which is exactly the fundamental estimate Kwon provides (8.5% above the risk-free rate). Since the APT equilibrium expected return equals the fundamentally expected return, the fund is fairly priced and no arbitrage (long/short) profit can be constructed.
Find weights \((w_H, w_L, w_U)\) such that the portfolio has GDP sensitivity 1 and inflation sensitivity 0:
\[\begin{cases} w_H + w_L + w_U = 1 \\ 1.25 w_H + 0.75 w_L + 1.0 w_U = 1 \\ 1.5 w_H + 1.25 w_L + 2.0 w_U = 0 \end{cases}\]
A <- rbind(c(1, 1, 1),
c(1.25, 0.75, 1.0),
c(1.5, 1.25, 2.0))
b <- c(1, 1, 0)
w <- solve(A, b)
round(setNames(w, c("High Growth", "Large Cap", "Utility")), 2)## High Growth Large Cap Utility
## 1.6 1.6 -2.2
Subtracting the first equation from the second gives \(w_H = w_L\); substituting into the system yields \(w_H = w_L = 1.6\) and \(w_U = 1 - 3.2 = -2.2\).
Answer: (a) −2.2 — the GDP Fund shorts the Utility Fund with a weight of −2.2.
Answer: (a) McCracken is correct and Stiles is wrong.
Stiles is wrong because the GDP Fund is not suitable for retirees who live off steady income: the fund is constructed to have unit exposure to real GDP risk, i.e., it is deliberately cyclical, and bearing factor risk means returns will fluctuate with the business cycle — exactly what income-dependent retirees should avoid. McCracken is correct: because the fund has full exposure to real GDP growth and zero exposure to inflation, it would benefit if supply-side macroeconomic policies succeed (higher real growth), making it a good choice for investors who want to bet on that outcome.
# Load libraries
library(tidyquant)
library(lubridate)
library(timetk)
library(purrr)
library(tidyverse)
library(PerformanceAnalytics) # Return.calculate(), table.AnnualizedReturns()
library(xts) # endpoints(), xts objectsDownload ETF daily data from Yahoo for SPY, QQQ, EEM, IWM, EFA, TLT, IYR and GLD from 2010 to the current date, and extract the adjusted daily closing prices.
tickers <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")
prices_raw <- tq_get(tickers, get = "stock.prices",
from = "2010-01-01", to = Sys.Date())
# Adjusted daily closing prices in wide xts format
prices_xts <- prices_raw %>%
select(symbol, date, adjusted) %>%
pivot_wider(names_from = symbol, values_from = adjusted) %>%
select(date, all_of(tickers)) %>%
tk_xts(date_var = date)
head(prices_xts)## SPY QQQ EEM IWM EFA TLT IYR
## 2010-01-04 84.79636 40.29078 30.35151 51.36657 35.12844 55.70951 26.76811
## 2010-01-05 85.02083 40.29078 30.57181 51.18992 35.15939 56.06933 26.83238
## 2010-01-06 85.08072 40.04777 30.63576 51.14178 35.30801 55.31875 26.82070
## 2010-01-07 85.43987 40.07380 30.45811 51.51911 35.17178 55.41182 27.06028
## 2010-01-08 85.72420 40.40363 30.69972 51.80010 35.45043 55.38696 26.87913
## 2010-01-11 85.84391 40.23871 30.63576 51.59136 35.74148 55.08308 27.00767
## 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 737.05 707.83 65.82 285.02 102.90 85.12 103.49 390.78
# Weekly simple (arithmetic) returns
ret_w_xts <- prices_xts[endpoints(prices_xts, on = "weeks")] %>%
Return.calculate(method = "discrete") %>%
na.omit()
# Monthly simple (arithmetic) returns
ret_m_xts <- prices_xts[endpoints(prices_xts, on = "months")] %>%
Return.calculate(method = "discrete") %>%
na.omit()
head(round(ret_w_xts, 4))## SPY QQQ EEM IWM EFA TLT IYR GLD
## 2010-01-15 -0.0081 -0.0150 -0.0289 -0.0130 -0.0035 0.0200 -0.0063 -0.0046
## 2010-01-22 -0.0390 -0.0369 -0.0558 -0.0306 -0.0557 0.0101 -0.0418 -0.0333
## 2010-01-29 -0.0167 -0.0310 -0.0336 -0.0262 -0.0258 0.0034 -0.0084 -0.0113
## 2010-02-05 -0.0068 0.0044 -0.0282 -0.0140 -0.0191 -0.0001 0.0032 -0.0121
## 2010-02-12 0.0129 0.0181 0.0333 0.0295 0.0052 -0.0195 -0.0076 0.0225
## 2010-02-19 0.0287 0.0245 0.0245 0.0334 0.0230 -0.0082 0.0502 0.0227
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 2010-02-26 0.0312 0.0460 0.0178 0.0448 0.0027 -0.0034 0.0546 0.0327
## 2010-03-31 0.0609 0.0771 0.0811 0.0823 0.0639 -0.0206 0.0975 -0.0044
## 2010-04-30 0.0155 0.0224 -0.0017 0.0568 -0.0280 0.0332 0.0639 0.0588
## 2010-05-28 -0.0795 -0.0739 -0.0939 -0.0754 -0.1119 0.0511 -0.0568 0.0305
## 2010-06-30 -0.0517 -0.0598 -0.0140 -0.0774 -0.0206 0.0580 -0.0467 0.0236
## 2010-07-30 0.0683 0.0726 0.1093 0.0673 0.1161 -0.0095 0.0940 -0.0509
Hint: use as_tibble(., rownames = 'date') or
tk_tbl(., rename_index = 'date').
ret_m_tbl <- ret_m_xts %>%
tk_tbl(rename_index = "date") %>%
# use the first day of each month as the date key (for merging with FF data)
mutate(date = floor_date(as.Date(date), "month"))
ret_m_tblSource: Ken French data library — Fama/French 3 factor monthly returns (Mkt-RF, SMB and HML), converted to xts data.
ff_url <- "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip"
tmp <- tempfile(fileext = ".zip")
download.file(ff_url, tmp, mode = "wb", quiet = TRUE)
csv_file <- unzip(tmp, exdir = tempdir())
ff3 <- read_csv(csv_file, skip = 3, col_types = cols(.default = "c")) %>%
rename(date = 1, Mkt.RF = `Mkt-RF`) %>%
filter(grepl("^\\d{6}$", date)) %>% # keep monthly rows only
mutate(date = ymd(paste0(date, "01")),
across(-date, ~ as.numeric(.) / 100)) # percent -> decimal digits
# convert into xts
ff3_xts <- tk_xts(ff3, date_var = date)
head(ff3_xts)## Mkt.RF SMB HML RF
## 1926-07-01 0.0289 -0.0255 -0.0239 0.0022
## 1926-08-01 0.0264 -0.0114 0.0381 0.0025
## 1926-09-01 0.0038 -0.0136 0.0005 0.0023
## 1926-10-01 -0.0327 -0.0014 0.0082 0.0032
## 1926-11-01 0.0254 -0.0011 -0.0061 0.0031
## 1926-12-01 0.0262 -0.0007 0.0006 0.0028
## Mkt.RF SMB HML RF
## 2025-11-01 -0.0013 0.0054 0.0357 0.0030
## 2025-12-01 -0.0036 -0.0103 0.0236 0.0034
## 2026-01-01 0.0103 0.0212 0.0386 0.0030
## 2026-02-01 -0.0117 0.0024 0.0265 0.0028
## 2026-03-01 -0.0518 0.0044 0.0335 0.0029
## 2026-04-01 0.0994 0.0013 -0.0127 0.0029
## [1] "2010-02-01" "2026-04-01"
Based on the CAPM (single-index) model, the covariance matrix of the 8-asset portfolio is estimated from historical 60-month returns (2010/02–2015/01):
\[\hat\Sigma_{CAPM} = \hat\beta\hat\beta'\,\hat\sigma_M^2 + \mathrm{diag}(\hat\sigma_{\varepsilon,1}^2,\dots,\hat\sigma_{\varepsilon,8}^2)\]
The global minimum variance (GMV) weights are \(w_{GMV} = \dfrac{\Sigma^{-1}\mathbf{1}}{\mathbf{1}'\Sigma^{-1}\mathbf{1}}\).
# --- helper: GMV weights from a covariance matrix ---
gmv_weights <- function(Sigma) {
ones <- rep(1, ncol(Sigma))
w <- solve(Sigma, ones)
w / sum(w)
}
# --- helper: covariance matrix from CAPM (single index) model ---
cov_capm <- function(window_df, assets) {
R <- as.matrix(window_df[, assets])
exc <- R - window_df$RF # excess returns of the 8 assets
mkt <- window_df$Mkt.RF # market excess return
fit <- lm(exc ~ mkt)
betas <- coef(fit)[2, ]
res_var <- apply(resid(fit), 2, var)
Sigma <- outer(betas, betas) * var(mkt) + diag(res_var)
dimnames(Sigma) <- list(assets, assets)
Sigma
}
# Estimation window: 2010/02 - 2015/01 (60 months)
win_60 <- merged %>%
filter(date >= ymd("2010-02-01"), date <= ymd("2015-01-01"))
nrow(win_60) # = 60 months## [1] 60
Sigma_capm <- cov_capm(win_60, tickers)
w_capm <- gmv_weights(Sigma_capm)
round(Sigma_capm * 1e4, 3) # covariance matrix (x 10^-4)## SPY QQQ EEM IWM EFA TLT IYR GLD
## SPY 13.967 14.591 17.258 18.285 15.808 -10.115 11.790 2.351
## QQQ 14.591 18.156 18.148 19.227 16.623 -10.636 12.397 2.472
## EEM 17.258 18.148 33.498 22.741 19.661 -12.580 14.663 2.923
## IWM 18.285 19.227 22.741 26.990 20.830 -13.328 15.535 3.097
## EFA 15.808 16.623 19.661 20.830 24.130 -11.523 13.431 2.678
## TLT -10.115 -10.636 -12.580 -13.328 -11.523 16.211 -8.594 -1.713
## IYR 11.790 12.397 14.663 15.535 13.431 -8.594 20.246 1.997
## GLD 2.351 2.472 2.923 3.097 2.678 -1.713 1.997 28.987
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.7748 -0.0130 -0.0361 -0.2029 -0.0357 0.4131 0.0373 0.0626
# Realized portfolio return on 2015/02
r_201502 <- merged %>% filter(date == ymd("2015-02-01"))
ret_capm_201502 <- sum(w_capm * as.numeric(r_201502[, tickers]))
ret_capm_201502## [1] -0.003340795
Answer: allocating the 8 assets with the CAPM-based GMV weights estimated on 2015/01, the realized portfolio return in 2015/02 is -0.0033 (= -0.33%).
Based on the Fama-French 3-factor model the covariance matrix is
\[\hat\Sigma_{FF3} = B\,\hat\Sigma_F\,B' + \mathrm{diag}(\hat\sigma_{\varepsilon,1}^2,\dots,\hat\sigma_{\varepsilon,8}^2),\]
where \(B\) is the \(8\times3\) matrix of factor loadings on (Mkt-RF, SMB, HML) and \(\hat\Sigma_F\) is the sample covariance matrix of the factors.
# --- helper: covariance matrix from the FF 3-factor model ---
cov_ff3 <- function(window_df, assets) {
R <- as.matrix(window_df[, assets])
exc <- R - window_df$RF
Fm <- as.matrix(window_df[, c("Mkt.RF", "SMB", "HML")])
fit <- lm(exc ~ Fm)
B <- t(coef(fit)[-1, ]) # 8 x 3 factor loadings
res_var <- apply(resid(fit), 2, var)
Sigma <- B %*% cov(Fm) %*% t(B) + diag(res_var)
dimnames(Sigma) <- list(assets, assets)
Sigma
}
Sigma_ff3 <- cov_ff3(win_60, tickers)
w_ff3 <- gmv_weights(Sigma_ff3)
round(Sigma_ff3 * 1e4, 3) # covariance matrix (x 10^-4)## SPY QQQ EEM IWM EFA TLT IYR GLD
## SPY 13.967 14.640 17.248 17.868 16.007 -10.078 11.787 2.038
## QQQ 14.640 18.156 18.440 18.697 17.131 -9.951 12.481 3.372
## EEM 17.248 18.440 33.498 22.700 19.801 -12.276 14.704 3.504
## IWM 17.868 18.697 22.700 26.990 19.433 -13.794 15.521 4.691
## EFA 16.007 17.131 19.801 19.433 24.130 -11.036 13.472 2.368
## TLT -10.078 -9.951 -12.276 -13.794 -11.036 16.211 -8.506 -0.718
## IYR 11.787 12.481 14.704 15.521 13.472 -8.506 20.246 2.163
## GLD 2.038 3.372 3.504 4.691 2.368 -0.718 2.163 28.987
## SPY QQQ EEM IWM EFA TLT IYR GLD
## 0.8828 -0.1425 -0.0431 -0.1153 -0.1037 0.4159 0.0368 0.0691
# Realized portfolio return on 2015/02
ret_ff3_201502 <- sum(w_ff3 * as.numeric(r_201502[, tickers]))
ret_ff3_201502## [1] -0.006559848
# Compare the two models' weights
tibble(asset = tickers,
`GMV weight (CAPM)` = round(w_capm, 4),
`GMV weight (FF3)` = round(w_ff3, 4))Answer: with the FF 3-factor-based GMV weights estimated on 2015/01, the realized portfolio return in 2015/02 is -0.0066 (= -0.66%).
Rolling-window backtest (implemented directly with R code instead of
the SIT package, which the question allows):
# Investment months: 2015/02 - 2026/05. The estimation window (t-60 ... t-1)
# needs the FF factors, but the realized return in month t only needs the
# asset returns, so we take r_t from ret_m_tbl (the FF factor file is
# published with a lag and currently ends one month earlier).
bt_dates <- ret_m_tbl %>%
filter(date >= ymd("2015-02-01"), date <= ymd("2026-05-01")) %>%
pull(date)
backtest <- map_dfr(bt_dates, function(t) {
win <- merged %>% filter(date < t) %>% slice_tail(n = 60) # t-60 ... t-1
r_t <- ret_m_tbl %>% filter(date == t)
w1 <- gmv_weights(cov_capm(win, tickers))
w2 <- gmv_weights(cov_ff3(win, tickers))
tibble(date = t,
GMV_CAPM = sum(w1 * as.numeric(r_t[, tickers])),
GMV_FF3 = sum(w2 * as.numeric(r_t[, tickers])))
})
head(backtest)cumret <- backtest %>%
mutate(`GMV (CAPM)` = cumprod(1 + GMV_CAPM) - 1,
`GMV (FF3)` = cumprod(1 + GMV_FF3) - 1) %>%
select(date, `GMV (CAPM)`, `GMV (FF3)`) %>%
pivot_longer(-date, names_to = "Model", values_to = "cum_return")
ggplot(cumret, aes(x = date, y = cum_return, color = Model)) +
geom_line(linewidth = 0.9) +
scale_y_continuous(labels = scales::percent) +
scale_color_manual(values = c("GMV (CAPM)" = "#d62728",
"GMV (FF3)" = "#1f77b4")) +
labs(title = "Cumulative returns of GMV portfolios, 2015/02 - 2026/05",
subtitle = "Rolling 60-month estimation window; CAPM vs Fama-French 3-factor covariance",
x = NULL, y = "Cumulative return") +
theme_tq() +
theme(legend.position = "bottom")bt_xts <- backtest %>% tk_xts(date_var = date)
# Annualized performance of the two GMV strategies
table.AnnualizedReturns(bt_xts, Rf = mean(merged$RF)) %>% round(4)## GMV_CAPM GMV_FF3
## 1.318 0.686
Comments: Both global-minimum-variance strategies deliver smooth, low-volatility performance, as designed. The two equity-factor models produce similar — but not identical — covariance estimates: the FF 3-factor model captures additional comovement through the SMB and HML factors, so its GMV weights and realized path differ somewhat from the single-index (CAPM) version. The cumulative-return chart above compares the two portfolios over the whole out-of-sample period 2015/02–2026/05.
Note: answers were written in R Markdown, knitted to HTML, and posted on RPubs as required.