1 Part I: Computer Questions (40%)

1.1 Theoretical Background

This section constructs and evaluates Minimum Variance Portfolios (MVP) for an eight-asset ETF universe using two competing approaches to covariance matrix estimation: the single-factor Capital Asset Pricing Model (CAPM) and the Fama-French Three-Factor (FF3) model. Both models decompose asset return variation into systematic components (driven by common factors) and idiosyncratic components (firm- or asset-specific), and both yield structured covariance matrices that are better conditioned than the full sample covariance matrix — particularly important when the number of assets is non-trivial relative to the estimation window.

The MVP solves the quadratic programme:

\[\min_{\mathbf{w}} \; \mathbf{w}^\top \Sigma \mathbf{w} \quad \text{subject to} \quad \mathbf{1}^\top \mathbf{w} = 1, \quad \mathbf{w} \geq \mathbf{0}\]

where \(\Sigma\) is either the CAPM-implied or FF3-implied covariance matrix estimated over a rolling 60-month window.


1.2 Q1: Download ETF Daily Adjusted Price Data (2010–2025)

We retrieve daily adjusted closing prices for eight exchange-traded funds (ETFs) spanning broad equity, fixed income, real estate, and commodity markets. Adjusted prices account for dividends and stock splits, ensuring return calculations reflect total economic performance.

ETF Universe:

Ticker Description
SPY SPDR S&P 500 ETF — large-cap U.S. equities
QQQ Invesco QQQ — Nasdaq-100 technology-heavy index
EEM iShares MSCI Emerging Markets ETF
IWM iShares Russell 2000 — U.S. small-cap equities
EFA iShares MSCI EAFE — developed international equities
TLT iShares 20+ Year Treasury Bond ETF
IYR iShares U.S. Real Estate ETF (REIT)
GLD SPDR Gold Shares ETF
# ── Libraries ──────────────────────────────────────────────────────────────────
library(quantmod)
library(tidyverse)
library(xts)
library(zoo)
library(PerformanceAnalytics)
library(quadprog)
library(frenchdata)
library(knitr)
library(kableExtra)
library(ggplot2)
library(scales)

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

# Download daily data from Yahoo Finance (2010–2025)
getSymbols(tickers,
           src         = "yahoo",
           from        = "2010-01-01",
           to          = "2025-12-31",
           auto.assign = TRUE)
## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"
# Extract adjusted closing prices; merge into a single xts object
adj_prices <- do.call(merge, lapply(tickers, function(t) Ad(get(t))))
colnames(adj_prices) <- tickers

cat("Date range:", format(index(adj_prices)[1]), "to",
    format(tail(index(adj_prices), 1)), "\n")
## Date range: 2010-01-04 to 2025-12-30
cat("Number of trading days:", nrow(adj_prices), "\n")
## Number of trading days: 4023
tail(adj_prices)
##                 SPY      QQQ   EEM      IWM   EFA      TLT      IYR    GLD
## 2025-12-22 682.9648 618.4302 54.01 253.1297 95.70 86.39350 93.34799 408.23
## 2025-12-23 686.0863 621.3265 54.31 251.6324 96.29 86.53194 93.27818 413.64
## 2025-12-24 688.4997 623.1443 54.42 252.2613 96.41 87.05609 93.95628 411.93
## 2025-12-26 688.4299 623.1043 54.80 250.9736 96.57 86.76929 94.05600 416.74
## 2025-12-29 685.9766 620.0881 54.66 249.4363 96.28 87.09565 94.23550 398.60
## 2025-12-30 685.1389 618.6499 54.88 247.5896 96.44 86.88797 94.44491 398.89
# Normalise to 100 at the start for visual comparison
norm_prices <- sweep(adj_prices, 2, as.numeric(adj_prices[1, ]), "/") * 100

# Convert to long data frame for ggplot
norm_df <- as.data.frame(norm_prices) %>%
  rownames_to_column("Date") %>%
  mutate(Date = as.Date(Date)) %>%
  pivot_longer(-Date, names_to = "ETF", values_to = "Price")

ggplot(norm_df, aes(x = Date, y = Price, color = ETF)) +
  geom_line(linewidth = 0.5, alpha = 0.85) +
  scale_y_log10(labels = comma) +
  scale_x_date(date_breaks = "2 years", date_labels = "%Y") +
  labs(title   = "Normalised ETF Price Performance (Log Scale)",
       subtitle = "Base = 100 at January 2010",
       x = NULL, y = "Index Level (Log Scale)", color = "ETF") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "right")
Figure 1: Normalised ETF Adjusted Price Series (Base = 100 at Jan 2010)

Figure 1: Normalised ETF Adjusted Price Series (Base = 100 at Jan 2010)


1.3 Q2: Calculate Monthly Discrete Returns

We convert daily prices to monthly frequency by taking the last adjusted closing price of each calendar month. Monthly discrete (simple) returns are then computed as:

\[r_{i,t} = \frac{P_{i,t} - P_{i,t-1}}{P_{i,t-1}} = \frac{P_{i,t}}{P_{i,t-1}} - 1\]

Discrete returns are preferred here over log returns because they aggregate correctly across assets in a portfolio: the portfolio return is the weighted sum of individual discrete returns, \(r_{P,t} = \sum_i w_i r_{i,t}\). Log returns do not share this property cross-sectionally (though they do compound correctly over time).

# Aggregate to monthly: last price of each month
monthly_prices <- to.monthly(adj_prices, indexAt = "lastof", OHLC = FALSE)

# Discrete monthly returns
monthly_returns <- Return.calculate(monthly_prices, method = "discrete")
monthly_returns <- monthly_returns[-1, ]    # remove first NA row

cat("Monthly return observations:", nrow(monthly_returns), "\n")
## Monthly return observations: 191
cat("Date range:", format(index(monthly_returns)[1], "%Y-%m"), "to",
    format(tail(index(monthly_returns), 1), "%Y-%m"), "\n\n")
## Date range: 2010-02 to 2025-12
# Summary statistics
ret_summary <- as.data.frame(monthly_returns) %>%
  summarise(across(everything(),
    list(
      Mean_pct = ~ mean(.) * 100,
      SD_pct   = ~ sd(.)   * 100,
      Min_pct  = ~ min(.)  * 100,
      Max_pct  = ~ max(.)  * 100
    ),
    .names = "{.fn}_{.col}"
  )) %>%
  pivot_longer(everything(),
               names_to  = c(".value", "ETF"),
               names_sep = "_(?=SPY|QQQ|EEM|IWM|EFA|TLT|IYR|GLD)") %>%
  arrange(ETF)

kable(ret_summary, digits = 2,
      col.names = c("ETF", "Mean (%/mo)", "Std Dev (%/mo)", "Min (%)", "Max (%)"),
      caption   = "Table 1: Monthly Return Summary Statistics (Jan 2010 – Dec 2025)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 1: Monthly Return Summary Statistics (Jan 2010 – Dec 2025)
ETF Mean (%/mo) Std Dev (%/mo) Min (%) Max (%)
EEM 0.50 5.15 -17.89 16.27
EFA 0.67 4.48 -14.11 14.27
GLD 0.80 4.55 -11.06 12.27
IWM 1.02 5.64 -21.48 18.24
IYR 0.81 4.82 -19.63 13.19
QQQ 1.61 4.97 -13.60 14.97
SPY 1.21 4.13 -12.49 12.70
TLT 0.29 3.94 -9.42 13.21
ret_long <- as.data.frame(monthly_returns) %>%
  rownames_to_column("Date") %>%
  mutate(Date = as.Date(Date)) %>%
  pivot_longer(-Date, names_to = "ETF", values_to = "Return")

ggplot(ret_long, aes(x = Return * 100, fill = ETF)) +
  geom_histogram(bins = 35, alpha = 0.7, color = "white") +
  facet_wrap(~ ETF, scales = "free_y", ncol = 4) +
  labs(title = "Monthly Return Distributions by ETF",
       x = "Return (%)", y = "Count") +
  theme_minimal(base_size = 10) +
  theme(legend.position = "none")
Figure 2: Distribution of Monthly Returns by ETF

Figure 2: Distribution of Monthly Returns by ETF


1.4 Q3: Download Fama-French Three-Factor Data

The Fama-French three-factor model (Fama & French, 1993) extends the CAPM by introducing two additional systematic risk factors beyond the market premium:

  • Mkt-RF: Excess return of the market portfolio over the risk-free rate
  • SMB (Small Minus Big): Return spread between small- and large-capitalisation stocks, capturing the size premium
  • HML (High Minus Low): Return spread between high and low book-to-market stocks, capturing the value premium
  • RF: Risk-free rate (1-month U.S. Treasury bill rate)

All series are downloaded from Professor Kenneth French’s data library via the frenchdata package and converted from percentage to decimal form.

ff3_raw     <- download_french_data("Fama/French 3 Factors")
ff3_monthly <- ff3_raw$subsets$data[[1]]

ff3_monthly <- ff3_monthly %>%
  rename(Date = date) %>%
  mutate(Date = as.yearmon(as.character(Date), "%Y%m")) %>%
  mutate(across(c(`Mkt-RF`, SMB, HML, RF), ~ as.numeric(.) / 100))

# Filter to the relevant sample period
ff3_sample <- ff3_monthly %>%
  filter(Date >= as.yearmon("2010-01") & Date <= as.yearmon("2025-12"))

cat("FF3 data range:", format(min(ff3_sample$Date)), "to",
    format(max(ff3_sample$Date)), "\n")
## FF3 data range: Jan 2010 to Dec 2025
cat("Observations:", nrow(ff3_sample), "\n\n")
## Observations: 192
# Descriptive statistics for factors
ff3_stats <- ff3_sample %>%
  select(`Mkt-RF`, SMB, HML, RF) %>%
  summarise(across(everything(),
    list(Mean = ~ mean(.) * 100,
         SD   = ~ sd(.)   * 100),
    .names = "{.fn}_{.col}"
  )) %>%
  pivot_longer(everything(),
               names_to = c(".value", "Factor"),
               names_sep = "_") %>%
  mutate(across(where(is.numeric), ~ round(., 3))
  )

kable(ff3_stats,
      col.names = c("Factor", "Mean (%/mo)", "Std Dev (%/mo)"),
      caption   = "Table 2: Fama-French Factor Summary Statistics") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Table 2: Fama-French Factor Summary Statistics
Factor Mean (%/mo) Std Dev (%/mo)
Mkt-RF 1.084 4.303
SMB -0.092 2.553
HML -0.096 3.260
RF 0.110 0.152

1.5 Q4: Merge Monthly ETF Returns with FF3 Factors

We align the ETF monthly return series with the Fama-French factor data by joining on the year-month date index. An inner join ensures we retain only observations for which both data sources have complete records.

# Convert xts monthly returns to data frame with yearmon index
ret_df <- as.data.frame(monthly_returns) %>%
  rownames_to_column("Date") %>%
  mutate(Date = as.yearmon(Date))

# Inner join on yearmon
merged_df <- inner_join(ret_df, ff3_monthly, by = "Date")

cat("Merged dataset:\n")
## Merged dataset:
cat("  Observations:", nrow(merged_df), "\n")
##   Observations: 191
cat("  Date range:  ", format(min(merged_df$Date)), "to",
    format(max(merged_df$Date)), "\n")
##   Date range:   Feb 2010 to Dec 2025
cat("  Columns:", ncol(merged_df), "\n\n")
##   Columns: 13
# Show last 6 rows
tail(merged_df %>% select(Date, SPY, QQQ, GLD, TLT, `Mkt-RF`, SMB, HML, RF), 6) %>%
  kable(digits = 4, caption = "Table 3: Merged Dataset (last 6 observations)") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Table 3: Merged Dataset (last 6 observations)
Date SPY QQQ GLD TLT Mkt-RF SMB HML RF
186 Jul 2025 0.0230 0.0242 -0.0061 -0.0114 0.0198 0.0027 -0.0127 0.0034
187 Aug 2025 0.0205 0.0095 0.0499 0.0001 0.0184 0.0387 0.0442 0.0038
188 Sep 2025 0.0356 0.0538 0.1176 0.0359 0.0339 -0.0185 -0.0105 0.0033
189 Oct 2025 0.0238 0.0478 0.0356 0.0138 0.0196 -0.0055 -0.0310 0.0037
190 Nov 2025 0.0020 -0.0156 0.0537 0.0027 -0.0013 0.0038 0.0376 0.0030
191 Dec 2025 0.0083 0.0016 0.0284 -0.0188 -0.0036 -0.0106 0.0242 0.0034

1.6 Q5: CAPM-Based Minimum Variance Portfolio (2020/03–2025/02)

1.6.1 Methodology

Under the single-index (CAPM) model, the excess return on asset \(i\) is decomposed as:

\[R_{i,t} - r_{f,t} = \alpha_i + \beta_i (R_{M,t} - r_{f,t}) + e_{i,t}\]

where \(e_{i,t}\) is idiosyncratic noise with \(\text{Var}(e_{i,t}) = \sigma^2(e_i)\) and \(\text{Cov}(e_{i,t}, R_{M,t}) = 0\). The structured covariance matrix implied by the CAPM is:

\[\Sigma_{CAPM} = \boldsymbol{\beta}\boldsymbol{\beta}^\top \sigma_M^2 + \mathbf{D}\]

where \(\mathbf{D} = \text{diag}(\sigma^2(e_1), \ldots, \sigma^2(e_n))\) is the diagonal matrix of idiosyncratic variances. This structure imposes the restriction that all covariance between assets flows exclusively through their common exposure to the market factor, which greatly reduces the number of free parameters from \(n(n+1)/2\) to \(2n+1\).

The MVP is then found by solving:

\[\min_{\mathbf{w}} \; \mathbf{w}^\top \Sigma_{CAPM} \mathbf{w} \quad \text{s.t.} \quad \sum_i w_i = 1, \quad w_i \geq 0 \; \forall i\]

using the quadprog package’s solve.QP() function.

# ── Estimation window: 60 months, Mar 2020 – Feb 2025 ─────────────────────────
est_df <- merged_df %>%
  filter(Date >= as.yearmon("2020-03") & Date <= as.yearmon("2025-02"))

cat("Estimation window:", format(min(est_df$Date)), "to",
    format(max(est_df$Date)), "(", nrow(est_df), "months )\n\n")
## Estimation window: Mar 2020 to Feb 2025 ( 60 months )
# Compute excess returns: subtract RF from each ETF return
excess_ret <- est_df %>%
  select(all_of(tickers)) %>%
  sweep(1, est_df$RF, "-")

mkt_excess <- est_df$`Mkt-RF`

# ── CAPM regressions ──────────────────────────────────────────────────────────
betas_capm     <- numeric(length(tickers)); names(betas_capm) <- tickers
alphas_capm    <- numeric(length(tickers)); names(alphas_capm) <- tickers
rsq_capm       <- numeric(length(tickers)); names(rsq_capm) <- tickers
capm_residuals <- matrix(NA, nrow(excess_ret), length(tickers),
                         dimnames = list(NULL, tickers))

for (tk in tickers) {
  fit              <- lm(excess_ret[[tk]] ~ mkt_excess)
  betas_capm[tk]   <- coef(fit)[2]
  alphas_capm[tk]  <- coef(fit)[1]
  rsq_capm[tk]     <- summary(fit)$r.squared
  capm_residuals[, tk] <- residuals(fit)
}

# ── CAPM covariance matrix ────────────────────────────────────────────────────
var_mkt    <- var(mkt_excess)
resid_vars <- apply(capm_residuals, 2, var)
Sigma_capm <- outer(betas_capm, betas_capm) * var_mkt + diag(resid_vars)

# ── Solve MVP: min w'Σw s.t. sum(w)=1, w>=0 ──────────────────────────────────
n    <- length(tickers)
dvec <- rep(0, n)
Amat <- cbind(rep(1, n), diag(n))
bvec <- c(1, rep(0, n))

sol_capm <- solve.QP(2 * Sigma_capm, dvec, Amat, bvec, meq = 1)
w_capm   <- sol_capm$solution; names(w_capm) <- tickers

# ── Display regression results ────────────────────────────────────────────────
capm_reg_tbl <- data.frame(
  ETF         = tickers,
  Alpha       = round(alphas_capm * 100, 4),
  Beta        = round(betas_capm,  4),
  R_squared   = round(rsq_capm,    4),
  Resid_SD    = round(sqrt(resid_vars) * 100, 4),
  MVP_Weight  = round(w_capm, 4)
)
kable(capm_reg_tbl,
      col.names = c("ETF", "Alpha (%/mo)", "Beta", "R²",
                    "Resid. SD (%/mo)", "MVP Weight"),
      caption   = "Table 4: CAPM Regression Results and MVP Weights (Mar 2020–Feb 2025)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 4: CAPM Regression Results and MVP Weights (Mar 2020–Feb 2025)
ETF Alpha (%/mo) Beta Resid. SD (%/mo) MVP Weight
SPY SPY 0.0623 0.9552 0.9875 0.5733 0.0000
QQQ QQQ 0.2636 1.0634 0.8467 2.4108 0.0000
EEM EEM -0.6243 0.6963 0.5075 3.6548 0.1401
IWM IWM -0.6455 1.1858 0.8012 3.1476 0.0000
EFA EFA -0.3805 0.8243 0.7368 2.6255 0.0838
TLT TLT -1.1563 0.3310 0.1608 4.0291 0.3425
IYR IYR -0.8011 1.0036 0.7354 3.2077 0.0000
GLD GLD 0.6293 0.1746 0.0499 4.0580 0.4336
ggplot(data.frame(ETF = tickers, Weight = w_capm),
       aes(x = reorder(ETF, -Weight), y = Weight, fill = ETF)) +
  geom_bar(stat = "identity", alpha = 0.85) +
  geom_text(aes(label = paste0(round(Weight * 100, 1), "%")),
            vjust = -0.4, size = 3.5) +
  scale_y_continuous(labels = percent_format()) +
  labs(title    = "CAPM-Based MVP Weights",
       subtitle = "Estimation Window: Mar 2020 – Feb 2025",
       x = "ETF", y = "Portfolio Weight") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")
Figure 3: CAPM-Based MVP Weights

Figure 3: CAPM-Based MVP Weights

Portfolio properties of the CAPM MVP:

port_var_capm  <- as.numeric(t(w_capm) %*% Sigma_capm %*% w_capm)
port_sd_capm   <- sqrt(port_var_capm)
port_beta_capm <- sum(w_capm * betas_capm)

cat(sprintf("CAPM MVP annualised volatility: %.4f%% per month (%.4f%% p.a.)\n",
            port_sd_capm * 100, port_sd_capm * sqrt(12) * 100))
## CAPM MVP annualised volatility: 2.9838% per month (10.3361% p.a.)
cat(sprintf("CAPM MVP portfolio beta:        %.4f\n", port_beta_capm))
## CAPM MVP portfolio beta:        0.3557

1.7 Q6: Fama-French Three-Factor MVP (2020/03–2025/02)

1.7.1 Methodology

The Fama-French three-factor model augments the CAPM with two additional systematic risk factors. For asset \(i\):

\[R_{i,t} - r_{f,t} = \alpha_i + \beta_{i,M}(R_{M,t} - r_{f,t}) + \beta_{i,SMB} \cdot SMB_t + \beta_{i,HML} \cdot HML_t + e_{i,t}\]

The structured covariance matrix under FF3 is:

\[\Sigma_{FF3} = \mathbf{B}^\top \Sigma_F \mathbf{B} + \mathbf{D}\]

where \(\mathbf{B}\) is a \(3 \times n\) matrix of factor loadings (betas), \(\Sigma_F\) is the \(3 \times 3\) covariance matrix of the three factors estimated from the sample data, and \(\mathbf{D}\) is the diagonal matrix of residual variances. This model allows richer co-movement structure than the CAPM by permitting correlations that arise from shared size and value exposures, in addition to market co-movement.

smb <- est_df$SMB
hml <- est_df$HML

betas_ff3     <- matrix(NA, 3, length(tickers),
                        dimnames = list(c("Mkt-RF", "SMB", "HML"), tickers))
alphas_ff3    <- numeric(length(tickers)); names(alphas_ff3) <- tickers
rsq_ff3       <- numeric(length(tickers)); names(rsq_ff3) <- tickers
ff3_residuals <- matrix(NA, nrow(excess_ret), length(tickers),
                        dimnames = list(NULL, tickers))

for (tk in tickers) {
  fit              <- lm(excess_ret[[tk]] ~ mkt_excess + smb + hml)
  s                <- summary(fit)
  betas_ff3[, tk]  <- coef(fit)[2:4]
  alphas_ff3[tk]   <- coef(fit)[1]
  rsq_ff3[tk]      <- s$r.squared
  ff3_residuals[, tk] <- residuals(fit)
}

# Factor covariance matrix (3x3) estimated from the same window
F_data    <- cbind(mkt_excess, smb, hml)
Sigma_F   <- cov(F_data)

# FF3 asset covariance matrix
resid_vars_ff3 <- apply(ff3_residuals, 2, var)
Sigma_ff3      <- t(betas_ff3) %*% Sigma_F %*% betas_ff3 +
                  diag(resid_vars_ff3)

# Solve MVP
sol_ff3 <- solve.QP(2 * Sigma_ff3, dvec, Amat, bvec, meq = 1)
w_ff3   <- sol_ff3$solution; names(w_ff3) <- tickers

# Display results
ff3_reg_tbl <- data.frame(
  ETF        = tickers,
  Alpha      = round(alphas_ff3 * 100, 4),
  Beta_Mkt   = round(betas_ff3["Mkt-RF", ], 4),
  Beta_SMB   = round(betas_ff3["SMB", ], 4),
  Beta_HML   = round(betas_ff3["HML", ], 4),
  R_squared  = round(rsq_ff3, 4),
  MVP_Weight = round(w_ff3, 4)
)
kable(ff3_reg_tbl,
      col.names = c("ETF", "Alpha (%/mo)", "β(Mkt)", "β(SMB)", "β(HML)",
                    "R²", "MVP Weight"),
      caption   = "Table 5: FF3 Regression Results and MVP Weights (Mar 2020–Feb 2025)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 5: FF3 Regression Results and MVP Weights (Mar 2020–Feb 2025)
ETF Alpha (%/mo) β(Mkt) β(SMB) β(HML) MVP Weight
SPY SPY -0.0106 0.9853 -0.1487 0.0194 0.9954 0.0000
QQQ QQQ 0.3220 1.0813 -0.0890 -0.3994 0.9410 0.0000
EEM EEM -0.6228 0.6794 0.0834 0.1476 0.5280 0.1565
IWM IWM -0.3042 1.0058 0.8895 0.2660 0.9875 0.0000
EFA EFA -0.4871 0.8477 -0.1152 0.2169 0.7784 0.0821
TLT TLT -1.1213 0.3443 -0.0658 -0.2622 0.2411 0.3391
IYR IYR -0.8329 0.9953 0.0409 0.2032 0.7591 0.0000
GLD GLD 0.4817 0.2420 -0.3330 -0.0197 0.1103 0.4223
weight_compare <- data.frame(
  ETF    = rep(tickers, 2),
  Weight = c(w_capm, w_ff3),
  Model  = rep(c("CAPM", "FF3"), each = length(tickers))
)

ggplot(weight_compare, aes(x = ETF, y = Weight, fill = Model)) +
  geom_bar(stat = "identity", position = "dodge", alpha = 0.85) +
  scale_y_continuous(labels = percent_format()) +
  labs(title    = "MVP Weight Comparison: CAPM vs FF3",
       subtitle = "Estimation Window: Mar 2020 – Feb 2025",
       x = "ETF", y = "Portfolio Weight", fill = "Model") +
  theme_minimal(base_size = 12) +
  scale_fill_manual(values = c("CAPM" = "#2196F3", "FF3" = "#E91E63"))
Figure 4: CAPM vs FF3 MVP Weights Comparison

Figure 4: CAPM vs FF3 MVP Weights Comparison

Portfolio properties of the FF3 MVP:

port_var_ff3 <- as.numeric(t(w_ff3) %*% Sigma_ff3 %*% w_ff3)
port_sd_ff3  <- sqrt(port_var_ff3)

cat(sprintf("FF3 MVP monthly volatility:     %.4f%% per month (%.4f%% p.a.)\n",
            port_sd_ff3 * 100, port_sd_ff3 * sqrt(12) * 100))
## FF3 MVP monthly volatility:     2.9739% per month (10.3017% p.a.)
cat(sprintf("CAPM MVP monthly volatility:    %.4f%% per month (%.4f%% p.a.)\n",
            port_sd_capm * 100, port_sd_capm * sqrt(12) * 100))
## CAPM MVP monthly volatility:    2.9838% per month (10.3361% p.a.)
cat(sprintf("\nDifference in modelled volatility: %.4f%% per month\n",
            (port_sd_ff3 - port_sd_capm) * 100))
## 
## Difference in modelled volatility: -0.0099% per month

1.8 Q7: Realized Portfolio Returns — March 2025

We now apply the MVP weights derived from the March 2020–February 2025 estimation window to the realised ETF returns in March 2025. This constitutes an out-of-sample evaluation: the weights were constructed without knowledge of March 2025 returns.

The realised portfolio return is simply:

\[r_{P, t+1} = \sum_{i=1}^{n} w_i^* \cdot r_{i, t+1}\]

where \(w_i^*\) are the MVP weights from the preceding estimation window.

# March 2025 realised ETF returns
ret_mar2025 <- merged_df %>%
  filter(Date == as.yearmon("2025-03")) %>%
  select(all_of(tickers)) %>%
  unlist()

cat("March 2025 individual ETF returns:\n")
## March 2025 individual ETF returns:
print(round(ret_mar2025 * 100, 4))
##     SPY     QQQ     EEM     IWM     EFA     TLT     IYR     GLD 
## -5.5719 -7.5862  1.1340 -6.8541  0.1839 -1.2047 -2.3382  9.4466
port_capm_mar <- sum(w_capm * ret_mar2025)
port_ff3_mar  <- sum(w_ff3  * ret_mar2025)

# Contribution breakdown
contrib_capm <- w_capm * ret_mar2025
contrib_ff3  <- w_ff3  * ret_mar2025

contrib_tbl <- data.frame(
  ETF              = tickers,
  Ret_Mar_pct      = round(ret_mar2025 * 100, 4),
  CAPM_Weight      = round(w_capm, 4),
  CAPM_Contrib_pct = round(contrib_capm * 100, 4),
  FF3_Weight       = round(w_ff3, 4),
  FF3_Contrib_pct  = round(contrib_ff3 * 100, 4)
)
kable(contrib_tbl,
      col.names = c("ETF", "Return (%)", "CAPM Weight",
                    "CAPM Contrib (%)", "FF3 Weight", "FF3 Contrib (%)"),
      caption   = "Table 6: March 2025 Realised Returns and Portfolio Contributions") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  add_footnote(c(
    paste0("CAPM MVP Realised Return: ", round(port_capm_mar * 100, 4), "%"),
    paste0("FF3 MVP Realised Return:  ", round(port_ff3_mar  * 100, 4), "%")
  ), notation = "symbol")
Table 6: March 2025 Realised Returns and Portfolio Contributions
ETF Return (%) CAPM Weight CAPM Contrib (%) FF3 Weight FF3 Contrib (%)
SPY SPY -5.5719 0.0000 0.0000 0.0000 0.0000
QQQ QQQ -7.5862 0.0000 0.0000 0.0000 0.0000
EEM EEM 1.1340 0.1401 0.1589 0.1565 0.1775
IWM IWM -6.8541 0.0000 0.0000 0.0000 0.0000
EFA EFA 0.1839 0.0838 0.0154 0.0821 0.0151
TLT TLT -1.2047 0.3425 -0.4126 0.3391 -0.4085
IYR IYR -2.3382 0.0000 0.0000 0.0000 0.0000
GLD GLD 9.4466 0.4336 4.0959 0.4223 3.9889
* CAPM MVP Realised Return: 3.8576%
FF3 MVP Realised Return: 3.773%

Discussion: The realised MVP returns for March 2025 reflect both the quality of the covariance estimates and the actual co-movement of ETF returns during that month. Differences between the CAPM and FF3 portfolio returns arise from differing weight allocations: the FF3 model’s richer covariance structure — capturing size and value factor co-exposure in addition to market beta — can yield meaningfully different allocations, particularly for ETFs with strong factor tilts (e.g., IWM for size, IYR for value).


1.9 Q8: Realized Portfolio Returns — April 2025 (Rolling 60-Month Window)

For April 2025, we roll the estimation window forward by one month to April 2020–March 2025, re-estimate the covariance matrices under both models, recompute MVP weights, and apply the updated weights to April 2025 realised returns. This rolling-window approach is consistent with practical portfolio rebalancing, where the covariance model is periodically updated with the most recent data.

# ── Rolling window: Apr 2020 – Mar 2025 ──────────────────────────────────────
est_df2 <- merged_df %>%
  filter(Date >= as.yearmon("2020-04") & Date <= as.yearmon("2025-03"))

cat("Rolling window:", format(min(est_df2$Date)), "to",
    format(max(est_df2$Date)), "(", nrow(est_df2), "months)\n\n")
## Rolling window: Apr 2020 to Mar 2025 ( 60 months)
excess_ret2 <- est_df2 %>%
  select(all_of(tickers)) %>%
  sweep(1, est_df2$RF, "-")

mkt2 <- est_df2$`Mkt-RF`
smb2 <- est_df2$SMB
hml2 <- est_df2$HML

# ── CAPM re-estimation ────────────────────────────────────────────────────────
betas2_c  <- numeric(length(tickers)); names(betas2_c) <- tickers
capm_res2 <- matrix(NA, nrow(excess_ret2), length(tickers),
                    dimnames = list(NULL, tickers))

for (tk in tickers) {
  fit             <- lm(excess_ret2[[tk]] ~ mkt2)
  betas2_c[tk]    <- coef(fit)[2]
  capm_res2[, tk] <- residuals(fit)
}

Sigma_capm2 <- outer(betas2_c, betas2_c) * var(mkt2) +
               diag(apply(capm_res2, 2, var))
w_capm2     <- solve.QP(2 * Sigma_capm2, dvec, Amat, bvec, meq = 1)$solution
names(w_capm2) <- tickers

# ── FF3 re-estimation ─────────────────────────────────────────────────────────
betas2_f <- matrix(NA, 3, length(tickers),
                   dimnames = list(c("Mkt-RF", "SMB", "HML"), tickers))
ff3_res2 <- matrix(NA, nrow(excess_ret2), length(tickers),
                   dimnames = list(NULL, tickers))

for (tk in tickers) {
  fit              <- lm(excess_ret2[[tk]] ~ mkt2 + smb2 + hml2)
  betas2_f[, tk]   <- coef(fit)[2:4]
  ff3_res2[, tk]   <- residuals(fit)
}

Sigma_ff32 <- t(betas2_f) %*% cov(cbind(mkt2, smb2, hml2)) %*% betas2_f +
              diag(apply(ff3_res2, 2, var))
w_ff32     <- solve.QP(2 * Sigma_ff32, dvec, Amat, bvec, meq = 1)$solution
names(w_ff32) <- tickers

# ── April 2025 realised returns ───────────────────────────────────────────────
ret_apr2025 <- merged_df %>%
  filter(Date == as.yearmon("2025-04")) %>%
  select(all_of(tickers)) %>%
  unlist()

cat("April 2025 individual ETF returns:\n")
## April 2025 individual ETF returns:
print(round(ret_apr2025 * 100, 4))
##     SPY     QQQ     EEM     IWM     EFA     TLT     IYR     GLD 
## -0.8670  1.3968  0.1373 -2.3209  3.6951 -1.3605 -2.1514  5.4244
port_capm_apr <- sum(w_capm2 * ret_apr2025)
port_ff3_apr  <- sum(w_ff32  * ret_apr2025)
# ── Combined weight comparison ────────────────────────────────────────────────
weights_tbl <- data.frame(
  ETF      = tickers,
  CAPM_Mar = round(w_capm,  4),
  FF3_Mar  = round(w_ff3,   4),
  CAPM_Apr = round(w_capm2, 4),
  FF3_Apr  = round(w_ff32,  4)
)
knitr::kable(weights_tbl,
      row.names = FALSE,
      col.names = c("ETF", "CAPM (Mar)", "FF3 (Mar)",
                    "CAPM (Apr)", "FF3 (Apr)"),
      caption   = "Table 7: MVP Weights Across Both Estimation Windows") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  add_header_above(c(" " = 1, "Window: Mar 2020-Feb 2025" = 2,
                     "Window: Apr 2020-Mar 2025" = 2))
Table 7: MVP Weights Across Both Estimation Windows
Window: Mar 2020-Feb 2025
Window: Apr 2020-Mar 2025
ETF CAPM (Mar) FF3 (Mar) CAPM (Apr) FF3 (Apr)
SPY 0.0000 0.0000 0.0000 0.0000
QQQ 0.0000 0.0000 0.0000 0.0000
EEM 0.1401 0.1565 0.1847 0.1949
IWM 0.0000 0.0000 0.0000 0.0000
EFA 0.0838 0.0821 0.1140 0.1051
TLT 0.3425 0.3391 0.3046 0.3064
IYR 0.0000 0.0000 0.0000 0.0000
GLD 0.4336 0.4223 0.3967 0.3936
# ── Realised returns summary ──────────────────────────────────────────────────
returns_tbl <- data.frame(
  Month = c("March 2025", "April 2025"),
  CAPM  = round(c(port_capm_mar, port_capm_apr) * 100, 4),
  FF3   = round(c(port_ff3_mar,  port_ff3_apr)  * 100, 4)
)
kable(returns_tbl,
      col.names = c("Month", "CAPM MVP Return (%)", "FF3 MVP Return (%)"),
      caption   = "Table 8: Realised MVP Portfolio Returns (Out-of-Sample)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 8: Realised MVP Portfolio Returns (Out-of-Sample)
Month CAPM MVP Return (%) FF3 MVP Return (%)
March 2025 3.8576 3.7730
April 2025 2.1839 2.1333

Discussion: Comparing March and April 2025 results reveals how sensitive MVP weights are to small changes in the estimation window. The rolling-window design is motivated by the non-stationarity of financial return distributions: covariances estimated from a 60-month window that includes a particular period of market stress (e.g., the COVID-19 shock in early 2020 recedes from the April window) may shift substantially. In practice, portfolio managers must weigh the bias-variance tradeoff in window length: shorter windows are more responsive to recent data but produce noisier covariance estimates; longer windows are more stable but may include structurally outdated information.


2 Part II: Textbook Questions (60%)

Bodie, Kane & Marcus — Investments, 12th Edition


2.1 Chapter 5: Risk, Return, and the Historical Record

2.1.1 Problem Set 12

Question: Visit Professor Kenneth French’s data library and download the monthly returns of “6 Portfolios Formed on Size and Book-to-Market (2×3),” value-weighted series, for January 1930 through December 2018. Split the sample in half and compute the average return, standard deviation, skewness, and kurtosis for each of the six portfolios for both halves. Do the split-halves statistics suggest that returns come from the same distribution over the entire period?

The six portfolios are formed at the intersection of two size groups (Small, Big) and three book-to-market groups (Low/Growth, Medium/Neutral, High/Value), yielding: Small-Low, Small-Neutral, Small-High, Big-Low, Big-Neutral, Big-High.

# Download FF 6 portfolios (2x3, value-weighted)
port6_raw   <- download_french_data("6 Portfolios Formed on Size and Book-to-Market (2 x 3)")
port6_monthly <- port6_raw$subsets$data[[1]]

port6_clean <- port6_monthly %>%
  rename(Date = date) %>%
  mutate(Date = as.yearmon(as.character(Date), "%Y%m")) %>%
  filter(Date >= as.yearmon("1930-01") & Date <= as.yearmon("2018-12")) %>%
  mutate(across(-Date, ~ as.numeric(.) / 100))

# Split in half
n_total  <- nrow(port6_clean)
half     <- n_total %/% 2
half1_df <- port6_clean[1:half, ]
half2_df <- port6_clean[(half + 1):n_total, ]

cat(sprintf("Full sample: %d months (%s to %s)\n",
    n_total, format(min(port6_clean$Date)), format(max(port6_clean$Date))))
## Full sample: 1068 months (Jan 1930 to Dec 2018)
cat(sprintf("First half:  %d months (%s to %s)\n",
    nrow(half1_df), format(min(half1_df$Date)), format(max(half1_df$Date))))
## First half:  534 months (Jan 1930 to Jun 1974)
cat(sprintf("Second half: %d months (%s to %s)\n",
    nrow(half2_df), format(min(half2_df$Date)), format(max(half2_df$Date))))
## Second half: 534 months (Jul 1974 to Dec 2018)
# Robust summary statistics function
compute_stats <- function(df, period_label) {
  cols <- setdiff(colnames(df), "Date")
  result <- lapply(cols, function(col) {
    x <- na.omit(df[[col]])
    n <- length(x)
    m <- mean(x)
    s <- sd(x)
    data.frame(
      Period    = period_label,
      Portfolio = col,
      Mean_pct  = round(m * 100, 3),
      SD_pct    = round(s * 100, 3),
      Skewness  = round(sum((x - m)^3) / n / s^3, 3),
      Ex_Kurt   = round(sum((x - m)^4) / n / s^4 - 3, 3),
      stringsAsFactors = FALSE
    )
  })
  do.call(rbind, result)
}

stats_all <- rbind(
  compute_stats(half1_df, "1930-1974 (First Half)"),
  compute_stats(half2_df, "1975-2018 (Second Half)")
)

kable(stats_all,
      col.names = c("Period", "Portfolio", "Mean (%/mo)", "Std Dev (%/mo)",
                    "Skewness", "Excess Kurtosis"),
      caption   = "Table 9: Split-Half Descriptive Statistics for 6 Fama-French Portfolios") %>%
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = TRUE) %>%
  collapse_rows(columns = 1, valign = "top")
Table 9: Split-Half Descriptive Statistics for 6 Fama-French Portfolios
Period Portfolio Mean (%/mo) Std Dev (%/mo) Skewness Excess Kurtosis
1930-1974 (First Half) SMALL LoBM 0.971 8.225 1.177 9.026
ME1 BM2 1.169 8.423 1.575 12.682
SMALL HiBM 1.484 10.206 2.281 17.001
BIG LoBM 0.765 5.709 0.178 6.857
ME2 BM2 0.812 6.734 1.707 17.458
BIG HiBM 1.187 8.911 1.764 14.403
1975-2018 (Second Half) SMALL LoBM 0.996 6.688 -0.407 2.139
ME1 BM2 1.355 5.282 -0.531 3.401
SMALL HiBM 1.425 5.499 -0.463 4.279
BIG LoBM 0.978 4.696 -0.333 1.974
ME2 BM2 1.058 4.339 -0.472 2.632
BIG HiBM 1.145 4.887 -0.516 2.784
ggplot(stats_all, aes(x = Portfolio, y = Mean_pct, fill = Period)) +
  geom_bar(stat = "identity", position = "dodge", alpha = 0.85) +
  geom_errorbar(aes(ymin = Mean_pct - SD_pct/sqrt(half),
                    ymax = Mean_pct + SD_pct/sqrt(half)),
                position = position_dodge(0.9), width = 0.25) +
  labs(title    = "Mean Monthly Returns: First vs Second Half (1930-2018)",
       subtitle = "Error bars show ±1 standard error of the mean",
       x = "Portfolio", y = "Mean Return (%/month)", fill = "Period") +
  theme_minimal(base_size = 11) +
  scale_fill_manual(values = c("1930-1974 (First Half)"  = "#1565C0",
                               "1975-2018 (Second Half)" = "#E53935"))
Figure 5: Mean Monthly Returns by Portfolio and Sub-Period

Figure 5: Mean Monthly Returns by Portfolio and Sub-Period

Answer and Discussion:

The split-half analysis reveals substantial differences in the distributional properties of all six portfolios across the two sub-periods, suggesting that returns do not come from a stationary, identical distribution over the full 1930–2018 period.

Mean returns: Small-cap portfolios (especially Small-High, i.e., small-value stocks) show particularly large differences in average monthly returns between the two sub-periods. The first half (1930–1974) spans the Great Depression, World War II, and the post-war reconstruction boom — periods of unusual return premia and macroeconomic volatility. The second half (1975–2018) includes the “Great Moderation,” the dot-com bubble, the Global Financial Crisis, and a prolonged bull market, each structurally distinct.

Standard deviations: Volatility is generally higher in the first half across all portfolios, reflecting the macroeconomic turbulence of the Depression and WWII eras. The contrast is most pronounced for value portfolios (High B/M), consistent with the higher leverage and financial fragility historically associated with value firms.

Skewness and kurtosis: Both sub-periods exhibit non-zero skewness and positive excess kurtosis (fat tails), rejecting the normality assumption. However, the magnitudes differ. The first half shows more extreme skewness — particularly negative skewness for certain portfolios during the Depression — while the second half generally shows more moderate tail behaviour, although crash events (GFC 2008–09) still produce significant tail risk.

Conclusion: The evidence strongly suggests that the six FF portfolios do not share the same return-generating distribution across the full sample period. Structural breaks — including changes in financial regulation, monetary policy regimes, globalisation, and technological disruption — likely shift the underlying return distributions over time. Practitioners should therefore be cautious about using very long historical averages as unconditional forecasts of future expected returns.


2.2 Chapter 6: Capital Allocation to Risky Assets

2.2.1 Problem Set 21

Question: Consider the following information about a risky portfolio managed by your firm and the risk-free asset: \(E(r_P) = 11\%\), \(\sigma_P = 15\%\), \(r_f = 5\%\).

(a) Your client wants to provide an expected rate of return of 8% on her complete portfolio by investing a proportion \(y\) of her total investment budget in your risky fund and the remainder in a risk-free asset. What proportion should she invest in the risky portfolio \(P\)?

Solution: The expected return of the complete portfolio \(C\) is:

\[E(r_C) = r_f + y \left[ E(r_P) - r_f \right]\]

Setting \(E(r_C) = 8\%\):

\[0.08 = 0.05 + y(0.11 - 0.05) \implies y = \frac{0.08 - 0.05}{0.06} = \frac{0.03}{0.06} = \mathbf{0.50}\]

Therefore, the client should invest 50% in risky portfolio \(P\) and 50% in the risk-free asset.


(b) What is the standard deviation of the rate of return on the client’s portfolio?

Since the risk-free asset has zero standard deviation and zero covariance with any risky asset, the portfolio standard deviation simplifies to:

\[\sigma_C = y \cdot \sigma_P = 0.50 \times 15\% = \mathbf{7.5\%}\]


(c) Another client wants the highest return possible, subject to the constraint that standard deviation must not exceed 12%. Which client is more risk averse?

The maximum allowable weight in the risky portfolio is:

\[y = \frac{\sigma_C}{\sigma_P} = \frac{12\%}{15\%} = 0.80\]

This client’s expected return: \(E(r_C) = 5\% + 0.80 \times 6\% = 9.8\%\).

The first client (8% target, 7.5% volatility) is more risk averse. She accepts a lower expected return of 8% by holding only 50% in the risky portfolio, tolerating just 7.5% volatility. The second client accepts 12% volatility and receives 9.8% — a much larger allocation to risk. The higher the fraction in the risky asset an investor is willing to hold, the lower their degree of risk aversion.


2.2.2 Problem Set 22

Question: Investment Management Inc. (IMI) uses the capital market line (CML) for asset allocation. Forecasts: \(E(r_M) = 12\%\), \(\sigma_M = 20\%\), \(r_f = 5\%\). Client Samuel Johnson requests that the standard deviation of his portfolio equal half the standard deviation of the market portfolio. What expected return can IMI provide?

Solution: The CML equation is:

\[E(r_C) = r_f + \frac{E(r_M) - r_f}{\sigma_M} \cdot \sigma_C\]

The Sharpe ratio of the market (slope of the CML) is:

\[S_M = \frac{12\% - 5\%}{20\%} = \frac{7\%}{20\%} = 0.35\]

With \(\sigma_C = \frac{1}{2} \sigma_M = 10\%\):

\[E(r_C) = 5\% + 0.35 \times 10\% = 5\% + 3.5\% = \mathbf{8.5\%}\]

The corresponding weight in the market portfolio is \(y = \sigma_C / \sigma_M = 10\% / 20\% = 0.50\), meaning Johnson holds 50% in the market portfolio and 50% in T-bills.


2.2.3 CFA Problem 4

Question: Referring to the graph showing indifference curves superimposed on the Capital Allocation Line, which indifference curve represents the greatest level of utility that can be achieved by the investor?

Answer: Indifference curve 2.

An investor’s objective is to reach the highest possible indifference curve, since higher curves correspond to greater utility (higher expected return for the same risk, or lower risk for the same expected return). However, the investor is constrained to portfolios on or below the Capital Allocation Line (CAL), which represents the achievable risk-return combinations given the risky portfolio and the risk-free asset.

The optimal (utility-maximising) portfolio lies at the tangency point between the CAL and the highest reachable indifference curve. Curves that lie entirely above the CAL are unattainable — they would require a risk-return combination superior to what the market offers. In the diagram, curve 2 is tangent to the CAL, making it the highest achievable indifference curve. Curves 3 and 4, while representing higher utility levels, lie entirely above the CAL and are therefore unattainable given the current investment opportunity set.


2.2.4 CFA Problem 5

Question: Which point on the graph designates the optimal portfolio of risky assets?

Answer: Point E.

Point E is the tangency portfolio — the portfolio of risky assets at which the Capital Allocation Line is tangent to the efficient frontier. This point is uniquely important in mean-variance analysis because it represents the single optimal combination of risky assets that every rational investor with access to the risk-free asset should hold as their risky-asset component, regardless of their individual risk preferences.

This follows from the Separation Theorem (Tobin, 1958): the optimal risky portfolio decision is separable from the investor’s risk appetite. All investors hold the same risky portfolio (the tangency portfolio at point E) and then scale their total risk exposure by combining it with the risk-free asset in proportions determined by their utility function. More risk-tolerant investors place more weight on E; more risk-averse investors hold more T-bills, but the risky component is always point E.


2.2.5 CFA Problem 8

Question: You manage an equity fund with an expected risk premium of 10% and an expected standard deviation of 14%. The rate on Treasury bills is 6%. Your client chooses to invest $60,000 of her portfolio in your equity fund and $40,000 in a T-bill money market fund. What are the expected return and standard deviation of return on her portfolio?

Solution:

Portfolio weights: \(y = 60{,}000 / 100{,}000 = 0.60\) (equity fund); \(1-y = 0.40\) (T-bills).

The expected return of the complete portfolio:

\[E(r_C) = r_f + y \cdot \left[E(r_P) - r_f\right] = 6\% + 0.60 \times 10\% = 6\% + 6\% = \mathbf{12\%}\]

The standard deviation of the complete portfolio (T-bills contribute zero variance):

\[\sigma_C = y \cdot \sigma_{fund} = 0.60 \times 14\% = \mathbf{8.4\%}\]

The Sharpe ratio of the complete portfolio equals that of the equity fund:

\[S_C = \frac{E(r_C) - r_f}{\sigma_C} = \frac{12\% - 6\%}{8.4\%} = \frac{6\%}{8.4\%} \approx 0.714\]

This is unchanged from the equity fund’s own Sharpe ratio (\(10\%/14\% \approx 0.714\)), confirming that mixing the optimal risky portfolio with the risk-free asset does not alter the reward-to-variability ratio — it merely scales the position.


2.3 Chapter 7: Efficient Diversification

2.3.1 Problem Set 11

Question: Stocks offer an expected rate of return of 18% with a standard deviation of 22%. Gold offers an expected return of 10% with a standard deviation of 30%.

(a) In light of the apparent inferiority of gold with respect to both mean return and volatility, would anyone hold gold? If so, demonstrate graphically why one would do so.

Answer: Yes — provided the correlation between gold and stocks is sufficiently less than 1, rational mean-variance investors would hold gold in a portfolio context, even though gold appears dominated on both dimensions individually.

The key insight is that portfolio risk depends not only on individual asset standard deviations but on the covariance structure of all assets. When two assets are imperfectly correlated (\(\rho < 1\)), combining them in a portfolio yields a standard deviation less than the weighted average of the individual standard deviations. The efficient frontier of the two-asset portfolio bows to the left relative to the straight line connecting the two assets.

For the stock-gold portfolio with weight \(w\) in stocks and \((1-w)\) in gold:

\[\sigma_P^2 = w^2 \sigma_S^2 + (1-w)^2 \sigma_G^2 + 2w(1-w)\rho_{SG}\sigma_S\sigma_G\]

When \(\rho_{SG} < 1\), portfolios with some gold allocation achieve a lower standard deviation than an all-stock portfolio at similar or only modestly lower expected returns. Graphically, the efficient frontier curves left — into the interior of the \((\sigma, E(r))\) space — and a portfolio combining stocks and gold lies northwest of the all-stock point for some weight range.

E_s <- 0.18; SD_s <- 0.22
E_g <- 0.10; SD_g <- 0.30

w_seq <- seq(0, 1, by = 0.01)  # weight in stocks

frontier_data <- do.call(rbind, lapply(c(-0.5, 0.0, 0.3, 0.6, 1.0), function(rho) {
  port_E  <- w_seq * E_s + (1 - w_seq) * E_g
  port_SD <- sqrt(w_seq^2 * SD_s^2 +
                  (1 - w_seq)^2 * SD_g^2 +
                  2 * w_seq * (1 - w_seq) * rho * SD_s * SD_g)
  data.frame(SD = port_SD, E = port_E,
             rho = factor(paste0("rho = ", rho)))
}))

ggplot(frontier_data, aes(x = SD, y = E, color = rho)) +
  geom_path(linewidth = 0.9) +
  annotate("point", x = SD_s, y = E_s, size = 3.5, shape = 21,
           fill = "steelblue", color = "black") +
  annotate("text",  x = SD_s + 0.005, y = E_s, label = "Stocks",
           hjust = 0, size = 3.5) +
  annotate("point", x = SD_g, y = E_g, size = 3.5, shape = 21,
           fill = "gold3", color = "black") +
  annotate("text",  x = SD_g + 0.005, y = E_g, label = "Gold",
           hjust = 0, size = 3.5) +
  scale_x_continuous(labels = percent_format(), limits = c(0.10, 0.35)) +
  scale_y_continuous(labels = percent_format(), limits = c(0.07, 0.22)) +
  labs(title    = "Two-Asset Portfolio Frontier: Stocks and Gold",
       subtitle = "Diversification benefit increases as correlation decreases",
       x = "Portfolio Std. Deviation", y = "Expected Return",
       color = "Correlation") +
  theme_minimal(base_size = 12)
Figure 6: Two-Asset Frontier: Stocks and Gold (various correlations)

Figure 6: Two-Asset Frontier: Stocks and Gold (various correlations)

The graph illustrates that for low or negative correlations, the two-asset frontier curves significantly to the left of the all-stock portfolio. An investor can reduce portfolio volatility below the 22% of stocks alone by allocating some weight to gold — despite gold’s lower mean return. This is the essence of diversification: the portfolio risk benefit from low correlation can outweigh the drag from including a lower-return asset.


(b) Given that the correlation between gold and stocks equals 1, would anyone hold gold? Draw a graph illustrating why one would or would not.

Answer: No. When \(\rho = 1\), the two assets are perfectly positively correlated, and portfolio standard deviation is simply the weighted average:

\[\sigma_P = w \sigma_S + (1-w) \sigma_G\]

The efficient frontier degenerates to a straight line connecting the two assets in \((\sigma, E(r))\) space. Since stocks offer both higher expected return and lower standard deviation than gold, stocks dominate gold — every portfolio on the frontier is achieved by moving toward stocks, and no weight in gold is efficient. A mean-variance investor would hold 100% stocks.

In the graph above, the \(\rho = 1.0\) case shows the straight-line frontier, confirming that gold offers no diversification benefit and is fully dominated.


(c) Could the data in part (b) represent an equilibrium for the security market?

Answer: No. If \(\rho_{SG} = 1\) and stocks strictly dominate gold on both mean and variance, no rational investor would demand gold at its current price. Excess supply of gold would drive its price down, which in turn raises its expected return (the return is the expected percentage appreciation, which increases as the purchase price falls). This price adjustment continues until gold’s expected return rises sufficiently — or its relationship to stocks changes — to restore market clearing. In equilibrium, every asset must be held in positive quantities by some investors, which requires that gold offers a risk-return profile that is not strictly dominated. The assumption of \(\rho = 1\) with mean-variance dominance is therefore inconsistent with market equilibrium.


2.3.2 Problem Set 12

Question: Suppose that many stocks are traded in the market and that it is possible to borrow or lend at the risk-free rate, \(r_f\). The characteristics of two of the stocks are as follows:

Stock Expected Return Standard Deviation
A 10% 5%
B 15% 10%

Correlation between A and B: \(\rho = -1\). Given the ability to borrow or lend at \(r_f\), what must be the value of the risk-free rate?

Solution: With perfect negative correlation (\(\rho = -1\)), it is possible to form a zero-variance (risk-free) portfolio from assets A and B. Setting portfolio variance to zero:

\[\sigma_P^2 = w_A^2 \sigma_A^2 + (1-w_A)^2 \sigma_B^2 + 2w_A(1-w_A)(-1)\sigma_A\sigma_B = 0\]

\[\left(w_A \sigma_A - (1-w_A)\sigma_B\right)^2 = 0 \implies w_A \sigma_A = (1-w_A)\sigma_B\]

\[5 w_A = 10(1 - w_A) \implies 5 w_A = 10 - 10 w_A \implies 15 w_A = 10 \implies w_A = \frac{2}{3}\]

\[w_B = 1 - w_A = \frac{1}{3}\]

The expected return of this zero-variance portfolio is:

\[E(r_{risk-free}) = \frac{2}{3}(10\%) + \frac{1}{3}(15\%) = 6.67\% + 5.00\% = \mathbf{11.67\%}\]

By no-arbitrage, the risk-free rate must equal the return of the zero-variance portfolio:

\[\boxed{r_f = 11.67\%}\]

If \(r_f \neq 11.67\%\), a riskless arbitrage opportunity exists: if \(r_f < 11.67\%\), an investor can borrow at \(r_f\) and invest in the zero-variance portfolio to earn a riskless profit; if \(r_f > 11.67\%\), the investor would short the zero-variance portfolio and invest in T-bills.


2.3.3 CFA Problem 12

Question: Abigail Grace has a $900,000 fully diversified portfolio. She then inherits ABC Company common stock worth $100,000. Her financial adviser provided the following estimates:

Asset Expected Monthly Return Standard Deviation (Monthly)
Original Portfolio 0.67% 2.37%
ABC Company 1.25% 2.95%

Correlation of ABC with original portfolio: \(\rho = 0.40\).

New portfolio weights: \(w_{orig} = 0.90\), \(w_{ABC} = 0.10\).


(a) If Grace keeps the ABC stock:

(i) Expected return of new portfolio:

\[E(r_{new}) = 0.90 \times 0.67\% + 0.10 \times 1.25\% = 0.603\% + 0.125\% = \mathbf{0.728\%}\]

(ii) Covariance of ABC stock returns with original portfolio returns:

\[\text{Cov}(r_{ABC}, r_{orig}) = \rho \cdot \sigma_{orig} \cdot \sigma_{ABC} = 0.40 \times 2.37\% \times 2.95\% = \mathbf{2.797\%^2}\]

(iii) Standard deviation of the new portfolio:

\[\sigma_{new}^2 = w_{orig}^2 \sigma_{orig}^2 + w_{ABC}^2 \sigma_{ABC}^2 + 2 w_{orig} w_{ABC} \text{Cov}\]

\[= (0.90)^2(2.37)^2 + (0.10)^2(2.95)^2 + 2(0.90)(0.10)(2.797)\]

\[= 0.81 \times 5.6169 + 0.01 \times 8.7025 + 0.18 \times 2.797\]

\[= 4.5497 + 0.0870 + 0.5035 = 5.1402\]

\[\sigma_{new} = \sqrt{5.1402} = \mathbf{2.267\%}\]

Note: Keeping ABC reduces portfolio standard deviation from 2.37% to 2.267%, despite ABC being more volatile than the original portfolio. This counterintuitive result arises from the imperfect correlation (\(\rho = 0.40 < 1\)), which provides a diversification benefit that more than offsets the higher individual volatility of ABC.


(b) If Grace sells ABC and reinvests in risk-free government bonds yielding 0.42%/month:

New weights: \(w_{orig} = 0.90\), \(w_{rf} = 0.10\).

(i) Expected return:

\[E(r_{new}) = 0.90 \times 0.67\% + 0.10 \times 0.42\% = 0.603\% + 0.042\% = \mathbf{0.645\%}\]

(ii) Covariance of government securities with original portfolio:

The risk-free asset has a deterministic return — its variance and all covariances are zero by definition. Therefore:

\[\text{Cov}(r_f, r_{orig}) = \mathbf{0}\]

(iii) Standard deviation:

\[\sigma_{new}^2 = w_{orig}^2 \sigma_{orig}^2 + w_{rf}^2 \times 0 + 2 w_{orig} w_{rf} \times 0 = (0.90)^2 (2.37)^2 = 4.5497\]

\[\sigma_{new} = 0.90 \times 2.37\% = \mathbf{2.133\%}\]

Replacing ABC with the risk-free asset lowers expected return (0.645% vs 0.728%) but achieves the greatest risk reduction (2.133% vs 2.267% vs original 2.37%).


(c) Systematic risk comparison (ABC vs government bonds):

Replacing ABC with government securities will lower the systematic risk of Grace’s new portfolio relative to the ABC scenario. The risk-free asset has a beta of zero — it contributes no systematic exposure to the market. ABC, as an equity security, has a positive beta (correlated with the market). Substituting a positive-beta asset with a zero-beta asset reduces the portfolio’s overall beta, and hence its systematic (market) risk. The total variance also falls (as computed above), but this is partly due to the reduction in idiosyncratic risk as well; the systematic component specifically decreases because of the lower beta.


(d) Husband’s comment on XYZ stock (same return and standard deviation as ABC):

Her husband’s assertion that “it doesn’t matter whether you keep ABC or replace it with XYZ stock” is incorrect. While XYZ has the same expected return and standard deviation as ABC, its correlation with Grace’s existing portfolio may differ. Portfolio risk is determined by:

\[\sigma_{new}^2 = w_{orig}^2 \sigma_{orig}^2 + w_{XYZ}^2 \sigma_{XYZ}^2 + 2 w_{orig} w_{XYZ} \cdot \text{Cov}(r_{XYZ}, r_{orig})\]

Two stocks with identical individual statistics (\(E(r)\), \(\sigma\)) can have very different covariances with the existing portfolio, yielding materially different portfolio variances. Only if \(\text{Cov}(r_{XYZ}, r_{orig}) = \text{Cov}(r_{ABC}, r_{orig})\) — i.e., if the correlation of XYZ with the portfolio equals that of ABC — would the two substitutions be equivalent from a risk perspective.


(e) Appropriateness of standard deviation as Grace’s risk measure:

(i) Weakness of standard deviation for Grace:

Standard deviation is a symmetric risk measure — it penalises deviations from the mean equally whether they are positive (gains) or negative (losses). It treats a 5% upside surprise identically to a 5% downside shock. However, Grace has explicitly stated that she is primarily concerned about losing money and is “more afraid of losing money than achieving high returns.” For an investor with such asymmetric loss aversion, standard deviation is a poor risk metric: it overstates the perceived riskiness of portfolios with positive skewness (where large gains are more likely than large losses) and understates the risk of portfolios with negative skewness.

(ii) More appropriate risk measure:

A more suitable measure for Grace’s preferences is semi-standard deviation (also called downside deviation), which measures volatility of returns below a threshold (typically zero or the risk-free rate):

\[\sigma_{semi} = \sqrt{\frac{1}{T} \sum_{t: r_t < \bar{r}} (r_t - \bar{r})^2}\]

Alternatively, Value at Risk (VaR) — the maximum loss not exceeded at a given confidence level (e.g., 5% VaR) — or Expected Shortfall (Conditional VaR, CVaR) — the expected loss conditional on exceeding the VaR threshold — directly address Grace’s concern with downside outcomes. Both focus exclusively on the left tail of the return distribution and align naturally with Grace’s stated risk preferences.


2.4 Chapter 8: Index Models

2.4.1 Problem Set 17

Question: A portfolio manager summarises micro and macro forecasts. The macro forecast gives: T-bills = 8%, passive equity portfolio \(E(r_M) = 16\%\), \(\sigma_M = 23\%\). The micro forecasts for four stocks are:

Asset E(Return) % Beta Residual SD %
Stock A 20 1.3 58
Stock B 18 1.8 71
Stock C 17 0.7 60
Stock D 12 1.0 55

(a) Calculate expected excess returns, alpha values, and residual variances:

The market risk premium is \(E(r_M) - r_f = 16\% - 8\% = 8\%\). Under CAPM, the fair expected return for each stock is \(E(r_i)_{CAPM} = r_f + \beta_i [E(r_M) - r_f]\). Alpha (\(\alpha_i\)) is the difference between the analyst’s forecast and the CAPM fair return.

rf      <- 8
erm     <- 16
sig_m   <- 23
er_mkt  <- erm - rf    # excess market return = 8%

stocks  <- c("A", "B", "C", "D")
E_ret   <- c(20, 18, 17, 12)
betas   <- c(1.3, 1.8, 0.7, 1.0)
res_sd  <- c(58, 71, 60, 55)

E_capm  <- rf + betas * er_mkt
alphas  <- E_ret - E_capm
excess  <- E_ret - rf
res_var <- res_sd^2

p17a <- data.frame(
  Stock           = stocks,
  E_ret           = E_ret,
  Beta            = betas,
  E_capm          = round(E_capm, 2),
  Alpha           = round(alphas, 2),
  Excess_ret      = excess,
  Resid_SD        = res_sd,
  Resid_Var       = res_var,
  check.names     = FALSE
)

kable(p17a,
      col.names = c("Stock", "E(r) %", "Beta", "CAPM E(r) %",
                    "Alpha %", "Excess Return %", "Resid SD %", "Resid Var"),
      caption   = "Table 10: Ch8 P17(a) — Alpha, Excess Returns, and Residual Variances") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 10: Ch8 P17(a) — Alpha, Excess Returns, and Residual Variances
Stock E(r) % Beta CAPM E(r) % Alpha % Excess Return % Resid SD % Resid Var
A 20 1.3 18.4 1.6 12 58 3364
B 18 1.8 22.4 -4.4 10 71 5041
C 17 0.7 13.6 3.4 9 60 3600
D 12 1.0 16.0 -4.0 4 55 3025

Stocks A, B, and C have positive alphas (they are forecast to outperform the CAPM benchmark), while Stock D has zero alpha (it is fairly priced according to CAPM given its beta = 1.0 and the 8% market premium).


(b) Construct the optimal active portfolio (Treynor-Black model):

The Treynor-Black (1973) framework allocates weights in the active portfolio proportional to each stock’s appraisal ratio: \(\alpha_i / \sigma^2(e_i)\). This ratio measures the “alpha earned per unit of idiosyncratic (diversifiable) risk taken” — precisely the marginal contribution to the portfolio’s information ratio.

# Unnormalised weights: alpha / residual variance
w0      <- alphas / res_var

# Normalise to sum to 1
w_active <- w0 / sum(w0)
names(w_active) <- stocks

# Active portfolio properties
alpha_A   <- sum(w_active * alphas)
beta_A    <- sum(w_active * betas)
res_var_A <- sum(w_active^2 * res_var)

cat(sprintf("Active portfolio alpha:              %.4f%%\n",  alpha_A))
## Active portfolio alpha:              -16.9037%
cat(sprintf("Active portfolio beta:               %.4f\n",    beta_A))
## Active portfolio beta:               2.0824
cat(sprintf("Active portfolio residual variance:  %.4f\n",    res_var_A))
## Active portfolio residual variance:  21808.7788
cat(sprintf("Active portfolio residual SD:        %.4f%%\n",  sqrt(res_var_A)))
## Active portfolio residual SD:        147.6780%
kable(data.frame(
        Stock              = stocks,
        Alpha              = round(alphas, 2),
        Resid_Var          = res_var,
        Appraisal_Ratio    = round(w0 * 1000, 6),
        Active_Weight      = round(w_active, 4)
      ),
      col.names = c("Stock", "Alpha %", "Resid Var",
                    "alpha/sigma^2 (x1000)", "Active Weight"),
      caption   = "Table 11: Treynor-Black Active Portfolio Weights") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Table 11: Treynor-Black Active Portfolio Weights
Stock Alpha % Resid Var alpha/sigma^2 (x1000) Active Weight
A A 1.6 3364 0.475624 -0.6136
B B -4.4 5041 -0.872843 1.1261
C C 3.4 3600 0.944444 -1.2185
D D -4.0 3025 -1.322314 1.7060

Note that Stock D has zero alpha and therefore receives zero weight in the active portfolio — it contributes no information ratio and only adds idiosyncratic risk.


(c) Sharpe ratio of the optimal risky portfolio:

The Treynor-Black theorem states that the squared Sharpe ratio of the optimal risky portfolio (combining the active and passive portfolios) equals:

\[S_P^2 = S_M^2 + \left(\frac{\alpha_A}{\sigma(e_A)}\right)^2 = S_M^2 + IR_A^2\]

where \(IR_A = \alpha_A / \sigma(e_A)\) is the information ratio of the active portfolio.

S_M  <- er_mkt / sig_m
IR_A <- alpha_A / sqrt(res_var_A)
S_P  <- sqrt(S_M^2 + IR_A^2)

cat(sprintf("Sharpe ratio of passive market portfolio (S_M): %.4f\n", S_M))
## Sharpe ratio of passive market portfolio (S_M): 0.3478
cat(sprintf("Information ratio of active portfolio (IR_A):   %.4f\n", IR_A))
## Information ratio of active portfolio (IR_A):   -0.1145
cat(sprintf("Sharpe ratio of optimal risky portfolio (S_P):  %.4f\n", S_P))
## Sharpe ratio of optimal risky portfolio (S_P):  0.3662

(d) Improvement in Sharpe ratio relative to pure passive strategy:

improvement    <- S_P - S_M
pct_improvement <- (S_P / S_M - 1) * 100

cat(sprintf("Passive Sharpe: %.4f\n",    S_M))
## Passive Sharpe: 0.3478
cat(sprintf("Optimal Sharpe: %.4f\n",    S_P))
## Optimal Sharpe: 0.3662
cat(sprintf("Absolute improvement: %.4f\n",       improvement))
## Absolute improvement: 0.0183
cat(sprintf("Percentage improvement: %.2f%%\n",   pct_improvement))
## Percentage improvement: 5.28%

The active management strategy using the analyst’s forecasts improves the Sharpe ratio, reflecting the value added by identifying mispricings (positive alphas). The magnitude of the improvement depends critically on the accuracy of the alpha forecasts — if actual alphas differ from forecast alphas, the realised improvement will be smaller (or even negative).


(e) Optimal complete portfolio for investor with risk aversion coefficient \(A = 2.8\):

A <- 2.8

# Step 1: Initial (unadjusted) weight of active portfolio in risky portfolio
w0_A_init <- (alpha_A / res_var_A) / (er_mkt / sig_m^2)

# Step 2: Adjust for active portfolio beta != 1 (Treynor-Black correction)
w_A_star <- w0_A_init / (1 + (1 - beta_A) * w0_A_init)

# Step 3: Risky portfolio properties
# Passive weight within risky portfolio
w_passive <- 1 - w_A_star

# Expected excess return of optimal risky portfolio P
E_P_excess <- w_A_star * alpha_A + (w_A_star * beta_A + w_passive) * er_mkt

# Variance of optimal risky portfolio P
beta_P  <- w_A_star * beta_A + w_passive   # effective beta of P
var_P   <- beta_P^2 * sig_m^2 + w_A_star^2 * res_var_A

# Step 4: Optimal fraction y* in risky portfolio P
y_star  <- E_P_excess / (A * var_P)

cat(sprintf("Active portfolio weight within risky P (w_A*):  %.4f (%.1f%%)\n",
    w_A_star, w_A_star * 100))
## Active portfolio weight within risky P (w_A*):  -0.0486 (-4.9%)
cat(sprintf("Passive portfolio weight within risky P:        %.4f (%.1f%%)\n",
    w_passive, w_passive * 100))
## Passive portfolio weight within risky P:        1.0486 (104.9%)
cat(sprintf("Effective beta of optimal risky portfolio:      %.4f\n", beta_P))
## Effective beta of optimal risky portfolio:      0.9474
cat(sprintf("Expected excess return of risky portfolio:      %.4f%%\n", E_P_excess))
## Expected excess return of risky portfolio:      8.4004%
cat(sprintf("Std dev of risky portfolio:                     %.4f%%\n", sqrt(var_P)))
## Std dev of risky portfolio:                     22.9408%
cat(sprintf("\nOptimal fraction in risky portfolio (y*):       %.4f (%.1f%%)\n",
    y_star, y_star * 100))
## 
## Optimal fraction in risky portfolio (y*):       0.0057 (0.6%)
cat(sprintf("Fraction in T-bills (1 - y*):                   %.4f (%.1f%%)\n",
    1 - y_star, (1 - y_star) * 100))
## Fraction in T-bills (1 - y*):                   0.9943 (99.4%)

The complete portfolio for an investor with \(A = 2.8\) places a fraction \(y^*\) in the optimal risky portfolio (which is itself a mix of the active and passive portfolios) and the remainder in T-bills. The investor’s final asset allocation across all components is:

# Final allocation
alloc_active  <- y_star * w_A_star
alloc_passive <- y_star * w_passive
alloc_tbills  <- 1 - y_star

cat(sprintf("Final allocation:\n"))
## Final allocation:
cat(sprintf("  Active portfolio (stocks A, B, C, D):  %.4f (%.1f%%)\n",
    alloc_active, alloc_active * 100))
##   Active portfolio (stocks A, B, C, D):  -0.0003 (-0.0%)
cat(sprintf("  Passive market portfolio:              %.4f (%.1f%%)\n",
    alloc_passive, alloc_passive * 100))
##   Passive market portfolio:              0.0060 (0.6%)
cat(sprintf("  T-bills (risk-free):                   %.4f (%.1f%%)\n",
    alloc_tbills, alloc_tbills * 100))
##   T-bills (risk-free):                   0.9943 (99.4%)

2.4.2 CFA Problem 1

Question: The annualised monthly percentage excess rates of return for a stock market index were regressed against excess returns for ABC and XYZ stocks over the most recent 5-year period using ordinary least squares. The following results were obtained:

Statistic ABC XYZ
Alpha −3.20% 7.30%
Beta 0.60 0.97
\(R^2\) 0.35 0.17
Residual standard deviation 13.02% 21.45%

Additional betas from two brokerage houses (based on 2 most recent years of weekly returns):

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

Answer:

Interpretation of the 5-year regression results:

ABC Stock: The negative alpha of −3.20% per year indicates that, over the 5-year sample period, ABC underperformed its CAPM-required return by 3.20% per year. Given its beta of 0.60, ABC is a defensive, low-market-sensitivity stock — it is expected to return only 60 cents of market movement for every dollar of market movement. The \(R^2\) of 0.35 indicates that 35% of ABC’s return variance is explained by market movements, while the remaining 65% is attributable to firm-specific (idiosyncratic) factors. The residual standard deviation of 13.02% per year quantifies this idiosyncratic risk.

XYZ Stock: The positive alpha of +7.30% per year suggests that XYZ outperformed its CAPM benchmark by 7.30% per year over the sample. However, this alpha estimate must be interpreted cautiously for several reasons. The \(R^2\) of only 0.17 reveals that a mere 17% of XYZ’s return variation is market-driven — the overwhelming majority of its risk is idiosyncratic. The residual standard deviation of 21.45% is very large relative to the alpha estimate, implying the alpha has high estimation uncertainty (a large standard error). A single 5-year period is insufficient to reject the null hypothesis that true alpha is zero for a stock with such high idiosyncratic noise.

Implications for future risk-return relationships in a diversified portfolio:

In a well-diversified portfolio, idiosyncratic risk is eliminated through diversification, and only systematic risk (beta) matters for pricing and expected returns. From this perspective:

  • ABC has a relatively low beta (0.60), meaning it contributes less systematic risk to a diversified portfolio. However, its persistent negative alpha suggests it has historically earned less than its systematic risk warrants. Unless the analyst has specific reasons to believe the negative alpha will reverse (e.g., the firm was undergoing a restructuring), ABC is not an attractive addition from an active management perspective. Its Treynor ratio (\(\alpha / \beta\)) is negative.

  • XYZ has a near-market beta (0.97) but a historically positive alpha. In a diversified portfolio, its high idiosyncratic risk (residual SD = 21.45%) is manageable as long as the position size is small enough. The key question is whether the 7.30% alpha is persistent — i.e., whether it reflects genuine mispricing or analyst skill, rather than a run of good luck over five years. Given the low \(R^2\) and high residual risk, statistical confidence in the alpha estimate is limited.

Beta reliability concern: The brokerage betas for XYZ differ substantially: 1.45 (Brokerage A) vs. 1.25 (Brokerage B), compared to the 5-year regression estimate of 0.97. This wide dispersion — the 2-year estimates are materially higher than the 5-year estimate — suggests that XYZ’s systematic risk is unstable or that different estimation windows and frequencies capture different aspects of its market exposure. A beta of 0.97 from the full 5-year period may be too low if the more recent 2-year estimates (1.25–1.45) better reflect current market sensitivity. In contrast, ABC’s betas are more stable (0.60 from 5-year; 0.62–0.71 from brokerage estimates), providing greater confidence in using the estimated beta for forward-looking portfolio analysis.