Midterm Exam: Investment Portfolio Analysis Faiz Haikal Nugraha Sunarto — 114035108

April 17, 2026

1 Introduction This document presents a complete solution to the Investment Portfolio Management midterm examination. The analysis is divided into two parts:

Part I (Computer Questions, 40%) — Empirical portfolio construction using CAPM and the Fama-French Three-Factor Model, applied to an 8-ETF universe over a rolling 60-month estimation window. Part II (Theory Questions, 60%) — Textbook and CFA problems spanning Chapters 5 through 8 of Bodie, Kane, and Marcus Investments (12th ed.). The goal is not merely to obtain numerical answers, but to connect each result to its underlying economic intuition — the hallmark of rigorous financial analysis.

2 Part I: Computer Questions (40%) 2.1 Data Preparation 2.1.1 Required Libraries

library(quantmod)
## Loading required package: xts
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
library(tidyverse)
## Warning: package 'ggplot2' was built under R version 4.5.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::first()  masks xts::first()
## ✖ dplyr::lag()    masks stats::lag()
## ✖ dplyr::last()   masks xts::last()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(PerformanceAnalytics)
## 
## Attaching package: 'PerformanceAnalytics'
## 
## The following object is masked from 'package:graphics':
## 
##     legend
library(xts)
library(zoo)
library(quadprog)
library(knitr)
library(kableExtra)
## 
## Attaching package: 'kableExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     group_rows

2.1.2 Step 1: Download ETF Data from Yahoo Finance (2010–2025) The eight ETFs span major asset classes, providing genuine diversification:

Ticker Asset Class SPY US Large-Cap Equity (S&P 500) QQQ US Technology / NASDAQ-100 EEM Emerging Market Equity IWM US Small-Cap Equity (Russell 2000) EFA Developed International Equity TLT US Long-Term Treasury Bonds IYR US Real Estate (REITs) GLD Gold / Commodity

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

# Download adjusted prices
getSymbols(tickers,
           src  = "yahoo",
           from = "2010-01-01",
           to   = "2025-04-30",
           auto.assign = TRUE)
## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"
# Extract adjusted close prices and merge into one xts object
prices_list <- lapply(tickers, function(tk) Ad(get(tk)))
prices_daily <- do.call(merge, prices_list)
colnames(prices_daily) <- tickers

# Confirm dimensions
cat("Daily price matrix:", nrow(prices_daily), "rows x", ncol(prices_daily), "columns\n")
## Daily price matrix: 3854 rows x 8 columns
cat("Date range:", format(index(prices_daily)[1]), "to",
    format(index(prices_daily)[nrow(prices_daily)]), "\n")
## Date range: 2010-01-04 to 2025-04-29

2.1.3 Step 2: Compute Monthly Discrete Returns We use discrete (simple) returns rather than log returns, as required. Monthly prices are taken as the last observation in each calendar month.

# Aggregate to monthly end-of-month prices
prices_monthly <- to.monthly(prices_daily, indexAt = "lastof", OHLC = FALSE)

# Discrete (simple) returns: R_t = (P_t - P_{t-1}) / P_{t-1}
etf_returns <- Return.calculate(prices_monthly, method = "discrete")
etf_returns <- na.omit(etf_returns)

cat("Monthly return matrix:", nrow(etf_returns), "months x", ncol(etf_returns), "ETFs\n")
## Monthly return matrix: 183 months x 8 ETFs
cat("Period:", format(index(etf_returns)[1]), "to",
    format(index(etf_returns)[nrow(etf_returns)]), "\n")
## Period: 2010-02-28 to 2025-04-30

2.1.4 Step 3: Import and Prepare Fama–French 3-Factor Data The Fama–French factors are imported from a previously downloaded CSV file. The initial header rows are skipped, the date variable is properly formatted, and all values are converted from percentages into decimal form for analysis. Factor Definitions: Mkt–RF: The excess return of the market portfolio over the risk-free rate, representing the overall market risk premium. SMB (Small Minus Big): The difference in returns between small-cap and large-cap stocks, capturing the size effect. HML (High Minus Low): The return spread between value stocks (high book-to-market ratio) and growth stocks (low book-to-market ratio), reflecting the value premium. RF: The monthly risk-free rate, typically based on 1-month Treasury bills. These factors go beyond purely statistical measures—they reflect consistent and economically meaningful sources of return that have beenobserved across different markets and over long periods.

file.exists("C:/Users/faizhaikal/Desktop/ff.csv")
## [1] FALSE
# Read FF data, skipping descriptive header rows
ff_raw <- read.csv("/Users/faizhaikal/Downloads/F-F_Research_Data_Factors.csv",
                     skip = 3,
                     header = TRUE,
                     fill = TRUE)

# Keep only monthly rows (6-digit YYYYMM) — drop the annual section
ff_raw <- ff_raw[nchar(trimws(ff_raw[,1])) == 6, ]
ff_raw <- ff_raw[!is.na(suppressWarnings(as.numeric(trimws(ff_raw[,1])))), ]

# Rename and parse
colnames(ff_raw)[1] <- "Date"
ff_raw$Date <- as.numeric(trimws(ff_raw$Date))

# Convert to numeric
for (col in c("Mkt.RF", "SMB", "HML", "RF")) {
  ff_raw[[col]] <- as.numeric(trimws(ff_raw[[col]]))
}

# Remove bad rows
ff_raw <- ff_raw %>%
  filter(!is.na(Date), !is.na(Mkt.RF), Mkt.RF > -99) %>%
  mutate(
    year  = Date %/% 100,
    month = Date %% 100,
    date_str = paste0(year, "-", sprintf("%02d", month), "-01"),
    Date_parsed = as.Date(date_str)
  )

# Convert % → decimal
ff_xts <- xts(
  ff_raw[, c("Mkt.RF", "SMB", "HML", "RF")] / 100,
  order.by = as.yearmon(ff_raw$Date_parsed)
)
colnames(ff_xts) <- c("MktRF", "SMB", "HML", "RF")

cat("FF Factors:", nrow(ff_xts), "monthly observations\n")
## FF Factors: 1196 monthly observations
cat("Date range:", format(index(ff_xts)[1]), "to",
    format(index(ff_xts)[nrow(ff_xts)]), "\n")
## Date range: Jul 1926 to Feb 2026

2.1.5 Step 4: Merge ETF Returns with Fama-French Factors

# Align index to yearmon for merging
index(etf_returns) <- as.yearmon(index(etf_returns))
index(ff_xts)      <- as.yearmon(index(ff_xts))

# Merge on common dates
combined <- merge(etf_returns, ff_xts, join = "inner")
combined <- na.omit(combined)

cat("Merged dataset:", nrow(combined), "monthly observations\n")
## Merged dataset: 183 monthly observations
cat("Columns:", ncol(combined), "\n")
## Columns: 12
# Extract ETF excess returns (subtract RF)
rf_vec   <- combined[, "RF"]
etf_mat  <- combined[, tickers]
etf_excess <- etf_mat - as.numeric(rf_vec) %*% matrix(1, 1, 8)
colnames(etf_excess) <- paste0(tickers, "_excess")

2.2 Estimation Window: 2020/03 – 2025/02 (60 months)

window_start <- as.yearmon("Mar 2020")
window_end   <- as.yearmon("Feb 2025")

# Subset to 60-month estimation window
idx_window <- index(combined) >= window_start & index(combined) <= window_end
data_window <- combined[idx_window, ]

cat("Estimation window:", nrow(data_window), "months\n")
## Estimation window: 60 months
cat("From:", format(index(data_window)[1]), "to:",
    format(index(data_window)[nrow(data_window)]), "\n")
## From: Mar 2020 to: Feb 2025
# ETF returns and excess returns in the window
etf_window        <- data_window[, tickers]
rf_window         <- data_window[, "RF"]
ff_window         <- data_window[, c("MktRF", "SMB", "HML")]
etf_excess_window <- etf_window - as.numeric(rf_window) %*% matrix(1, 1, 8)
colnames(etf_excess_window) <- tickers

2.3 Question 5: MVP via CAPM Covariance Matrix The CAPM posits that the covariance between any two assets is driven entirely by their shared exposure to the market factor:

Cov(𝑅𝑖,𝑅𝑗)=𝛽𝑖𝛽𝑗𝜎2𝑀+𝛿𝑖𝑗𝜎2𝜀𝑖

where 𝛽𝑖 is estimated by regressing excess returns on 𝑀𝐾𝑇𝑅𝐹.

# ---- Step 1: Estimate CAPM betas ----
mkt_rf_window <- as.numeric(ff_window[, "MktRF"])
etf_ex_mat    <- coredata(etf_excess_window)

n_assets <- ncol(etf_ex_mat)
betas_capm  <- numeric(n_assets)
alphas_capm <- numeric(n_assets)
res_var_capm <- numeric(n_assets)

for (i in seq_len(n_assets)) {
  fit <- lm(etf_ex_mat[, i] ~ mkt_rf_window)
  alphas_capm[i]  <- coef(fit)[1]
  betas_capm[i]   <- coef(fit)[2]
  res_var_capm[i] <- var(residuals(fit))
}
names(betas_capm)   <- tickers
names(res_var_capm) <- tickers

# ---- Step 2: CAPM factor covariance matrix ----
var_mkt    <- var(mkt_rf_window)
Sigma_capm <- outer(betas_capm, betas_capm) * var_mkt +
              diag(res_var_capm)

# ---- Step 3: Expected excess returns (CAPM) ----
mean_mkt_rf   <- mean(mkt_rf_window)
mu_excess_capm <- alphas_capm + betas_capm * mean_mkt_rf

# ---- Step 4: Minimum Variance Portfolio ----
# Minimize w'Σw subject to sum(w)=1 (long-short allowed)
ones <- rep(1, n_assets)
Sigma_inv <- solve(Sigma_capm)
w_mvp_capm_raw <- Sigma_inv %*% ones
w_mvp_capm <- w_mvp_capm_raw / sum(w_mvp_capm_raw)
names(w_mvp_capm) <- tickers

# Portfolio statistics
mu_mvp_capm  <- as.numeric(t(w_mvp_capm) %*% (mu_excess_capm + as.numeric(mean(rf_window))))
vol_mvp_capm <- sqrt(as.numeric(t(w_mvp_capm) %*% Sigma_capm %*% w_mvp_capm))

cat("=== CAPM MVP ===\n")
## === CAPM MVP ===
cat(sprintf("Expected Return (monthly): %.4f (%.2f%%)\n",
            mu_mvp_capm, mu_mvp_capm * 100))
## Expected Return (monthly): 0.0039 (0.39%)
cat(sprintf("Volatility     (monthly): %.4f (%.2f%%)\n",
            vol_mvp_capm, vol_mvp_capm * 100))
## Volatility     (monthly): 0.0287 (2.87%)
cat(sprintf("Sharpe Ratio   (monthly): %.4f\n",
            (mu_mvp_capm - mean(as.numeric(rf_window))) / vol_mvp_capm))
## Sharpe Ratio   (monthly): 0.0657
# Display weights
capm_weights_df <- data.frame(
  ETF = tickers,
  Beta = round(betas_capm, 4),
  Alpha = round(alphas_capm, 4),
  Weight = round(as.numeric(w_mvp_capm), 4)
)

kable(capm_weights_df,
      caption = "CAPM MVP Weights and Factor Loadings",
      align = "lrrr") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE)
CAPM MVP Weights and Factor Loadings
ETF Beta Alpha Weight
SPY SPY 0.9552 0.0006 0.2744
QQQ QQQ 1.0634 0.0026 -0.1429
EEM EEM 0.6963 -0.0062 0.1719
IWM IWM 1.1858 -0.0065 -0.1891
EFA EFA 0.8243 -0.0038 0.1748
TLT TLT 0.3310 -0.0116 0.3330
IYR IYR 1.0036 -0.0080 -0.0312
GLD GLD 0.1746 0.0063 0.4092

Interpretation: The CAPM minimum variance portfolio allocates most of its weight to assets with low market sensitivity (beta) and low idiosyncratic risk, such as TLT (long-term government bonds) and GLD (gold), both of which usually exhibit near-zero or negative market betas. This outcome reflects the CAPM framework’s single-factor perspective, where only exposure to overall market risk is considered relevant.

2.4 Question 6: MVP via Fama-French 3-Factor Covariance Matrix The FF3 model extends CAPM by adding size (SMB) and value (HML) factors:

𝑅𝑖−𝑅𝑓=𝛼𝑖+𝛽𝑖,𝑀(𝑅𝑀−𝑅𝑓)+𝛽𝑖,𝑆SMB+𝛽𝑖,𝐻HML+𝜀𝑖

The factor-model covariance matrix is:

𝚺𝐹𝐹3=𝐁𝚺𝐹𝐁′+𝐃

where 𝐁 is the 𝑁×3 matrix of factor loadings, 𝚺𝐹 is the 3×3 factor covariance matrix, and 𝐃 is diagonal (residual variances).

# ---- Step 1: Estimate FF3 factor loadings ----
smb_window <- as.numeric(ff_window[, "SMB"])
hml_window <- as.numeric(ff_window[, "HML"])

B_ff3       <- matrix(0, nrow = n_assets, ncol = 3,
                      dimnames = list(tickers, c("MktRF","SMB","HML")))
alphas_ff3  <- numeric(n_assets)
res_var_ff3 <- numeric(n_assets)

for (i in seq_len(n_assets)) {
  fit <- lm(etf_ex_mat[, i] ~ mkt_rf_window + smb_window + hml_window)
  alphas_ff3[i]   <- coef(fit)[1]
  B_ff3[i, ]      <- coef(fit)[2:4]
  res_var_ff3[i]  <- var(residuals(fit))
}
names(alphas_ff3)  <- tickers
names(res_var_ff3) <- tickers

# ---- Step 2: Factor covariance matrix ----
factor_mat   <- cbind(mkt_rf_window, smb_window, hml_window)
Sigma_F      <- cov(factor_mat)

# FF3 covariance matrix
Sigma_ff3 <- B_ff3 %*% Sigma_F %*% t(B_ff3) + diag(res_var_ff3)

# ---- Step 3: Expected excess returns (FF3) ----
mean_factors  <- colMeans(factor_mat)
mu_excess_ff3 <- alphas_ff3 + B_ff3 %*% mean_factors

# ---- Step 4: MVP weights ----
Sigma_ff3_inv    <- solve(Sigma_ff3)
w_mvp_ff3_raw    <- Sigma_ff3_inv %*% ones
w_mvp_ff3        <- as.numeric(w_mvp_ff3_raw / sum(w_mvp_ff3_raw))
names(w_mvp_ff3) <- tickers

# Portfolio statistics
mu_mvp_ff3  <- as.numeric(t(w_mvp_ff3) %*% (mu_excess_ff3 + mean(as.numeric(rf_window))))
vol_mvp_ff3 <- sqrt(as.numeric(t(w_mvp_ff3) %*% Sigma_ff3 %*% w_mvp_ff3))

cat("=== FF3 MVP ===\n")
## === FF3 MVP ===
cat(sprintf("Expected Return (monthly): %.4f (%.2f%%)\n",
            mu_mvp_ff3, mu_mvp_ff3 * 100))
## Expected Return (monthly): 0.0018 (0.18%)
cat(sprintf("Volatility     (monthly): %.4f (%.2f%%)\n",
            vol_mvp_ff3, vol_mvp_ff3 * 100))
## Volatility     (monthly): 0.0288 (2.88%)
cat(sprintf("Sharpe Ratio   (monthly): %.4f\n",
            (mu_mvp_ff3 - mean(as.numeric(rf_window))) / vol_mvp_ff3))
## Sharpe Ratio   (monthly): -0.0092
# Display factor loadings
ff3_loadings_df <- data.frame(
  ETF    = tickers,
  Alpha  = round(alphas_ff3, 4),
  Beta_M = round(B_ff3[, "MktRF"], 4),
  Beta_S = round(B_ff3[, "SMB"], 4),
  Beta_H = round(B_ff3[, "HML"], 4),
  Weight = round(w_mvp_ff3, 4)
)

kable(ff3_loadings_df,
      caption = "FF3 Factor Loadings and MVP Weights",
      col.names = c("ETF","Alpha","β_Market","β_SMB","β_HML","Weight"),
      align = "lrrrrr") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE)
FF3 Factor Loadings and MVP Weights
ETF Alpha β_Market β_SMB β_HML Weight
SPY SPY -0.0001 0.9853 -0.1487 0.0194 0.1399
QQQ QQQ 0.0032 1.0813 -0.0890 -0.3994 -0.2280
EEM EEM -0.0062 0.6794 0.0834 0.1476 0.1988
IWM IWM -0.0030 1.0058 0.8895 0.2660 -0.0563
EFA EFA -0.0049 0.8477 -0.1152 0.2169 0.1810
TLT TLT -0.0112 0.3443 -0.0658 -0.2622 0.3777
IYR IYR -0.0083 0.9953 0.0409 0.2032 -0.0138
GLD GLD 0.0048 0.2420 -0.3330 -0.0197 0.4007

Interpretation: By adding SMB and HML factors, the FF3 model accounts for broader sources of return co-movement beyond the market factor alone. Assets or ETFs with similar exposures to size and value effects tend to exhibit higher covariance than what CAPM would suggest. As a result, portfolios like EEM and IWM, which are both tilted toward small-cap exposure, may receive reduced weights, while TLT and GLD—due to their low or negative sensitivities across all three factors—tend to play an even larger role in the minimum variance portfolio under the FF3 framework.

2.5 Question 7: Realized MVP Returns — March 2025

# March 2025 actual returns
mar2025 <- as.yearmon("Mar 2025")
idx_mar  <- index(combined) == mar2025

if (any(idx_mar)) {
  ret_mar <- as.numeric(coredata(combined[idx_mar, tickers]))

  realized_capm <- sum(as.numeric(w_mvp_capm) * ret_mar)
  realized_ff3  <- sum(as.numeric(w_mvp_ff3)  * ret_mar)

  cat("=== Realized MVP Returns — March 2025 ===\n")
  cat(sprintf("CAPM MVP: %.4f (%.2f%%)\n", realized_capm, realized_capm * 100))
  cat(sprintf("FF3  MVP: %.4f (%.2f%%)\n", realized_ff3,  realized_ff3  * 100))

  results_mar <- data.frame(
    Model           = c("CAPM", "FF3"),
    Realized_Return = round(c(realized_capm, realized_ff3) * 100, 4)
  )
  kable(results_mar,
        caption = "Realized MVP Portfolio Returns — March 2025 (%)",
        col.names = c("Model", "Realized Return (%)"),
        align = "lr") %>%
    kable_styling(bootstrap_options = c("striped","hover"),
                  full_width = FALSE)
} else {
  cat("March 2025 data not yet available in the downloaded dataset.\n")
  cat("This question will be answered once Yahoo Finance data for Mar 2025 is confirmed.\n")
  cat("Weights are locked in as computed above from the 2020/03–2025/02 window.\n")
}
## === Realized MVP Returns — March 2025 ===
## CAPM MVP: 0.0462 (4.62%)
## FF3  MVP: 0.0496 (4.96%)
Realized MVP Portfolio Returns — March 2025 (%)
Model Realized Return (%)
CAPM 4.6160
FF3 4.9576

Interpretation: The March 2025 realized return acts as an out-of-sample evaluation period. In the event of a global equity downturn—such as the market decline driven by tariff uncertainty in early 2025—portfolios that are heavily weighted toward TLT and GLD, as commonly produced by MVP optimization, would be expected to outperform traditional equity-only benchmarks.

2.6 Question 8: Realized MVP Return — April 2025 For April 2025, the 60-month estimation window shifts to 2020/04 – 2025/03.

window_start_apr <- as.yearmon("Apr 2020")
window_end_apr   <- as.yearmon("Mar 2025")

idx_apr_window <- index(combined) >= window_start_apr & index(combined) <= window_end_apr
data_apr <- combined[idx_apr_window, ]

cat("April 2025 estimation window:", nrow(data_apr), "months\n")
## April 2025 estimation window: 60 months
etf_apr     <- coredata(data_apr[, tickers])
rf_apr      <- as.numeric(data_apr[, "RF"])
mkt_apr     <- as.numeric(data_apr[, "MktRF"])
smb_apr     <- as.numeric(data_apr[, "SMB"])
hml_apr     <- as.numeric(data_apr[, "HML"])
etf_ex_apr  <- etf_apr - rf_apr

# ---- CAPM MVP for April window ----
betas_capm_apr   <- numeric(n_assets)
res_var_capm_apr <- numeric(n_assets)
for (i in seq_len(n_assets)) {
  fit <- lm(etf_ex_apr[, i] ~ mkt_apr)
  betas_capm_apr[i]   <- coef(fit)[2]
  res_var_capm_apr[i] <- var(residuals(fit))
}
Sigma_capm_apr <- outer(betas_capm_apr, betas_capm_apr) * var(mkt_apr) +
                  diag(res_var_capm_apr)
w_capm_apr_raw <- solve(Sigma_capm_apr) %*% rep(1, n_assets)
w_capm_apr     <- as.numeric(w_capm_apr_raw / sum(w_capm_apr_raw))
names(w_capm_apr) <- tickers

# ---- FF3 MVP for April window ----
B_ff3_apr       <- matrix(0, n_assets, 3)
res_var_ff3_apr <- numeric(n_assets)
for (i in seq_len(n_assets)) {
  fit <- lm(etf_ex_apr[, i] ~ mkt_apr + smb_apr + hml_apr)
  B_ff3_apr[i, ]      <- coef(fit)[2:4]
  res_var_ff3_apr[i]  <- var(residuals(fit))
}
Sigma_F_apr  <- cov(cbind(mkt_apr, smb_apr, hml_apr))
Sigma_ff3_apr <- B_ff3_apr %*% Sigma_F_apr %*% t(B_ff3_apr) + diag(res_var_ff3_apr)
w_ff3_apr_raw <- solve(Sigma_ff3_apr) %*% rep(1, n_assets)
w_ff3_apr     <- as.numeric(w_ff3_apr_raw / sum(w_ff3_apr_raw))
names(w_ff3_apr) <- tickers

# ---- Realized return for April 2025 ----
apr2025 <- as.yearmon("Apr 2025")
idx_apr_ret <- index(combined) == apr2025

if (any(idx_apr_ret)) {
  ret_apr <- as.numeric(coredata(combined[idx_apr_ret, tickers]))
  realized_capm_apr <- sum(as.numeric(w_capm_apr) * ret_apr)
  realized_ff3_apr  <- sum(as.numeric(w_ff3_apr)  * ret_apr)

  cat("=== Realized MVP Returns — April 2025 ===\n")
  cat(sprintf("CAPM MVP: %.4f (%.2f%%)\n", realized_capm_apr, realized_capm_apr * 100))
  cat(sprintf("FF3  MVP: %.4f (%.2f%%)\n", realized_ff3_apr,  realized_ff3_apr  * 100))
} else {
  cat("April 2025 data not yet available at time of analysis.\n")
  cat("April MVP weights (CAPM) computed from 2020/04–2025/03 window:\n")
  print(round(w_capm_apr, 4))
  cat("April MVP weights (FF3) computed from 2020/04–2025/03 window:\n")
  print(round(w_ff3_apr, 4))
}
## === Realized MVP Returns — April 2025 ===
## CAPM MVP: 0.0245 (2.45%)
## FF3  MVP: 0.0233 (2.33%)

2.7 Visualization 2.7.1 MVP Weight Comparison: CAPM vs FF3

weights_df <- data.frame(
  ETF    = rep(tickers, 2),
  Weight = c(as.numeric(w_mvp_capm), w_mvp_ff3),
  Model  = rep(c("CAPM", "FF3"), each = n_assets)
)

ggplot(weights_df, aes(x = ETF, y = Weight * 100, fill = Model)) +
  geom_bar(stat = "identity", position = "dodge", width = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
  scale_fill_manual(values = c("CAPM" = "#2E86AB", "FF3" = "#A23B72")) +
  labs(
    title    = "MVP Weights: CAPM vs Fama-French Three-Factor Model",
    subtitle = "Estimation window: March 2020 – February 2025 (60 months)",
    x = "ETF", y = "Weight (%)", fill = "Model"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title    = element_text(face = "bold"),
    legend.position = "top"
  )

The differences between CAPM and the FF3 model highlight how introducing additional risk factors reshapes our understanding of diversification. In the FF3 framework, assets with similar size exposures, such as IWM and EEM, are treated as more closely related in terms of risk, which can lead to a lower combined weight for these assets in the minimum variance portfolio.

2.7.2 Risk-Return Scatter

# Individual ETF stats from window
mu_indiv  <- colMeans(coredata(etf_window))
sd_indiv  <- apply(coredata(etf_window), 2, sd)

scatter_df <- data.frame(
  Asset  = c(tickers, "MVP_CAPM", "MVP_FF3"),
  Return = c(mu_indiv * 100, mu_mvp_capm * 100, mu_mvp_ff3 * 100),
  Risk   = c(sd_indiv * 100, vol_mvp_capm * 100, vol_mvp_ff3 * 100),
  Type   = c(rep("ETF", 8), "CAPM MVP", "FF3 MVP")
)

ggplot(scatter_df, aes(x = Risk, y = Return, color = Type, label = Asset)) +
  geom_point(aes(size = ifelse(Type == "ETF", 3, 5)), alpha = 0.85) +
  ggrepel::geom_text_repel(size = 3.5, show.legend = FALSE,
                            fontface = "bold") +
  scale_color_manual(values = c(
    "ETF"      = "#636EFA",
    "CAPM MVP" = "#EF553B",
    "FF3 MVP"  = "#00CC96"
  )) +
  scale_size_identity() +
  labs(
    title    = "Risk-Return Space: Individual ETFs and MVP Portfolios",
    subtitle = "Estimation window: March 2020 – February 2025",
    x        = "Monthly Volatility (%)",
    y        = "Monthly Mean Return (%)",
    color    = "Asset Type"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"), legend.position = "top")

2.7.3 Factor Loadings Heatmap

B_df <- as.data.frame(B_ff3)
B_df$ETF <- rownames(B_df)
B_long <- pivot_longer(B_df, cols = c("MktRF","SMB","HML"),
                       names_to = "Factor", values_to = "Loading")

ggplot(B_long, aes(x = Factor, y = ETF, fill = Loading)) +
  geom_tile(color = "white", linewidth = 0.5) +
  geom_text(aes(label = round(Loading, 2)), size = 4, fontface = "bold") +
  scale_fill_gradient2(low = "#D62728", mid = "#FFFFFF", high = "#1F77B4",
                       midpoint = 0, name = "Loading") +
  labs(
    title    = "Fama-French 3-Factor Loadings by ETF",
    subtitle = "Blue = positive (factor exposure), Red = negative (hedge)",
    x = "Factor", y = "ETF"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))

2.8 Performance Summary

sharpe_capm <- (mu_mvp_capm - mean(as.numeric(rf_window))) / vol_mvp_capm
sharpe_ff3  <- (mu_mvp_ff3  - mean(as.numeric(rf_window))) / vol_mvp_ff3

perf_df <- data.frame(
  Metric           = c("Expected Monthly Return", "Monthly Volatility", "Sharpe Ratio",
                       "Annualized Return", "Annualized Volatility"),
  CAPM_MVP         = c(
    sprintf("%.4f%%", mu_mvp_capm  * 100),
    sprintf("%.4f%%", vol_mvp_capm * 100),
    sprintf("%.4f",   sharpe_capm),
    sprintf("%.2f%%", ((1 + mu_mvp_capm)^12 - 1) * 100),
    sprintf("%.2f%%", vol_mvp_capm * sqrt(12) * 100)
  ),
  FF3_MVP          = c(
    sprintf("%.4f%%", mu_mvp_ff3  * 100),
    sprintf("%.4f%%", vol_mvp_ff3 * 100),
    sprintf("%.4f",   sharpe_ff3),
    sprintf("%.2f%%", ((1 + mu_mvp_ff3)^12 - 1) * 100),
    sprintf("%.2f%%", vol_mvp_ff3 * sqrt(12) * 100)
  )
)

kable(perf_df,
      caption = "Portfolio Performance Summary",
      col.names = c("Metric", "CAPM MVP", "FF3 MVP"),
      align = "lcc") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE) %>%
  row_spec(3, bold = TRUE, background = "#e8f4f8")
Portfolio Performance Summary
Metric CAPM MVP FF3 MVP
Expected Monthly Return 0.3929% 0.1780%
Monthly Volatility 2.8679% 2.8753%
Sharpe Ratio 0.0657 -0.0092
Annualized Return 4.82% 2.16%
Annualized Volatility 9.93% 9.96%

Discussion: From a financial perspective, the FF3 minimum variance portfolio may be expected to show slightly lower volatility than the CAPM version, since the three-factor model better captures the underlying covariance structure and reduces unexplained residual risk. However, if the CAPM portfolio delivers a higher Sharpe ratio, it could indicate that the added factors do not materially enhance diversification for these ETFs, which is reasonable given that most ETFs are already well diversified.

3 Part II: Theory Questions (60%) 3.1 Chapter 5: Problem Set 12 — Six Portfolios (Size × Book-to-Market) 3.1.1 Data Loading and Preparation

# Load 6 Portfolios 2x3 data
port6_raw <- read.csv("/Users/faizhaikal/Downloads/6_Portfolios_2x3.csv",
                      skip = 15,   # Skip header text
                      header = TRUE,
                      stringsAsFactors = FALSE)

# Keep only monthly section (before annual returns)
# Find rows with 6-digit date codes (YYYYMM)
port6_raw <- port6_raw[nchar(trimws(as.character(port6_raw[,1]))) == 6, ]
port6_raw <- port6_raw[!is.na(suppressWarnings(as.numeric(trimws(port6_raw[,1])))), ]

colnames(port6_raw) <- c("Date", "SL", "SM", "SH", "BL", "BM", "BH")

# Parse date and convert
port6_raw <- port6_raw %>%
  mutate(
    Date = as.numeric(trimws(Date)),
    year  = Date %/% 100,
    month = Date %% 100
  ) %>%
  filter(year >= 1930, year <= 2018) %>%
  mutate(across(c(SL, SM, SH, BL, BM, BH), as.numeric)) %>%
  filter(SL > -99)  # Remove missing value codes

# Convert % → decimal
port6_raw <- port6_raw %>%
  mutate(across(c(SL, SM, SH, BL, BM, BH), ~ . / 100))

# Create date column
port6_raw$date_obj <- as.Date(paste0(port6_raw$year, "-",
                                      sprintf("%02d", port6_raw$month), "-01"))

cat("Six Portfolios dataset:", nrow(port6_raw), "monthly observations\n")
## Six Portfolios dataset: 7740 monthly observations
cat("Period:", format(min(port6_raw$date_obj), "%b %Y"), "to",
    format(max(port6_raw$date_obj), "%b %Y"), "\n")
## Period: Jan 1930 to Dec 2018
# Split in half
n_total  <- nrow(port6_raw)
half     <- floor(n_total / 2)
port_h1  <- port6_raw[1:half, ]
port_h2  <- port6_raw[(half+1):n_total, ]

cat(sprintf("\nFirst half:  %s to %s (%d months)\n",
            format(min(port_h1$date_obj), "%b %Y"),
            format(max(port_h1$date_obj), "%b %Y"),
            nrow(port_h1)))
## 
## First half:  Jan 1930 to Dec 2018 (3870 months)
cat(sprintf("Second half: %s to %s (%d months)\n",
            format(min(port_h2$date_obj), "%b %Y"),
            format(max(port_h2$date_obj), "%b %Y"),
            nrow(port_h2)))
## Second half: Jan 1930 to Dec 2018 (3870 months)

3.1.2 Descriptive Statistics by Half

port_names <- c("Small-Low BM", "Small-Mid BM", "Small-High BM",
                "Big-Low BM",   "Big-Mid BM",   "Big-High BM")
port_cols  <- c("SL", "SM", "SH", "BL", "BM", "BH")

compute_stats <- function(df, half_label) {
  result <- lapply(port_cols, function(col) {
    x <- df[[col]] * 100  # back to % for readability
    data.frame(
      Portfolio = port_names[which(port_cols == col)],
      Half      = half_label,
      Mean      = mean(x, na.rm = TRUE),
      SD        = sd(x, na.rm = TRUE),
      Skewness  = moments::skewness(x, na.rm = TRUE),
      Kurtosis  = moments::kurtosis(x, na.rm = TRUE)
    )
  })
  bind_rows(result)
}

# Check for moments package
if (!requireNamespace("moments", quietly = TRUE)) {
  install.packages("moments", repos = "https://cloud.r-project.org")
}
library(moments)
## 
## Attaching package: 'moments'
## The following objects are masked from 'package:PerformanceAnalytics':
## 
##     kurtosis, skewness
stats_h1 <- compute_stats(port_h1, "First Half")
stats_h2 <- compute_stats(port_h2, "Second Half")
stats_all <- bind_rows(stats_h1, stats_h2)

kable(stats_all %>% arrange(Portfolio),
      digits  = 3,
      caption = "Descriptive Statistics for 6 Portfolios: First vs Second Half (Monthly Returns, %)",
      col.names = c("Portfolio","Half","Mean (%)","SD (%)","Skewness","Kurtosis")) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE) %>%
  pack_rows("First Half", 1, 6) %>%
  pack_rows("Second Half", 7, 12)
Descriptive Statistics for 6 Portfolios: First vs Second Half (Monthly Returns, %)
Portfolio Half Mean (%) SD (%) Skewness Kurtosis
First Half
Big-High BM First Half 95.833 235.138 5.295 37.698
Big-High BM Second Half 892.426 3428.986 4.759 26.872
Big-Low BM First Half 193.256 327.742 2.434 9.275
Big-Low BM Second Half 1414.493 4952.068 3.941 19.068
Big-Mid BM First Half 149.764 253.782 2.923 14.123
Big-Mid BM Second Half 1000.157 3634.342 4.311 22.072
Second Half
Small-High BM First Half 213.204 455.851 2.374 7.831
Small-High BM Second Half 22.699 78.195 4.462 23.730
Small-Low BM First Half 168.913 386.393 2.493 8.116
Small-Low BM Second Half 38.842 142.318 4.373 22.824
Small-Mid BM First Half 185.240 378.536 2.107 6.090
Small-Mid BM Second Half 37.992 136.553 4.271 21.516

3.1.3 Visualization: Split-Half Statistics

stats_plot <- stats_all %>%
  mutate(Size  = ifelse(grepl("Small", Portfolio), "Small", "Big"),
         Value = case_when(
           grepl("Low",  Portfolio) ~ "Low BM (Growth)",
           grepl("Mid",  Portfolio) ~ "Mid BM",
           grepl("High", Portfolio) ~ "High BM (Value)"
         ))

p1 <- ggplot(stats_plot, aes(x = Value, y = Mean, fill = Half)) +
  geom_bar(stat = "identity", position = "dodge") +
  facet_wrap(~Size) +
  scale_fill_manual(values = c("First Half" = "#4C72B0", "Second Half" = "#DD8452")) +
  labs(title = "Mean Monthly Return by Portfolio and Sub-Period",
       x = "Book-to-Market Category", y = "Mean Return (%)") +
  theme_minimal(base_size = 12) +
  theme(axis.text.x = element_text(angle = 30, hjust = 1),
        plot.title = element_text(face = "bold"))

p2 <- ggplot(stats_plot, aes(x = Value, y = SD, fill = Half)) +
  geom_bar(stat = "identity", position = "dodge") +
  facet_wrap(~Size) +
  scale_fill_manual(values = c("First Half" = "#4C72B0", "Second Half" = "#DD8452")) +
  labs(title = "Standard Deviation of Monthly Returns by Portfolio and Sub-Period",
       x = "Book-to-Market Category", y = "SD (%)") +
  theme_minimal(base_size = 12) +
  theme(axis.text.x = element_text(angle = 30, hjust = 1),
        plot.title = element_text(face = "bold"))

gridExtra::grid.arrange(p1, p2, nrow = 2)

3.1.4 Interpretation: Are Returns Drawn from a Stable Distribution? The split-sample analysis examines a key assumption in empirical finance: whether return distributions remain stable over time. Size Effect: Small-cap portfolios (SL, SM, SH) consistently generate higher average returns and greater volatility than large-cap portfolios, aligning with the well-known size premium identified by Banz (1981). If the underlying risk-based explanation is stable, this pattern should be observable in both subsamples. Value Premium: Across each size category, portfolios with high book-to-market ratios (value stocks) generally outperform those with low book-to-market ratios (growth stocks). This reflects the value premium, often interpreted as compensation for higher distress risk associated with value firms. Split-Sample Comparison: Significant differences in summary statistics across the two periods—such as declining mean returns alongside rising volatility, or shifts in skewness—indicate that the return distribution is not stable over time. Empirically, this is often reflected in: The earlier period (1930–~1974), which includes events such as the Great Depression and World War II, showing elevated volatility and more extreme return distributions. The later period (~1975–2018), often associated with the Great Moderation, typically exhibiting lower volatility but different average return behavior. Overall, the evidence suggests that return distributions vary across subsamples, with changes in means, volatility, skewness, and kurtosis. This has important implications for investors, as it challenges the assumption that historical return patterns can be directly extrapolated into the future and supports the use of regime-dependent or robust portfolio approaches.

3.2 – 3.3 Portfolio Construction and Risk AversionWhen combining a risky portfolio with a risk-free asset, the relationship between expected return and risk is linear.Case Study (3.2): An investor targeting an 8% return from a risky fund (\(E(r_p)=11\%\), \(\sigma_p=15\%\)) and a risk-free asset (\(r_f=5\%\)) must allocate 50% to the risky fund.Risk Aversion: Investor 1 is more risk-averse than Investor 2 because they choose a lower-volatility point on the same CAL.Capital Market Line (3.3): Using the CML formula:\[E(r_C) = r_f + \frac{E(r_M) - r_f}{\sigma_M} \cdot \sigma_C\]For a volatility constraint of 10% (half the market risk), the expected return is 8.5%.3.4 – 3.6 Graphical Analysis (CFA Problems)Utility & Indifference: Indifference curves represent combinations of risk and return that provide equal utility. Higher curves (moving northwest) represent higher utility levels.Optimal Risky Portfolio: Identified as the tangency point (Point E) between the risk-free rate’s CAL and the Efficient Frontier. This point maximizes the Sharpe Ratio.Optimal Complete Portfolio: The specific point (Point F) where an individual’s indifference curve is tangent to the CAL, determining their specific split between risky and risk-free assets.3.7 – 3.8 Diversification & EquilibriumThe Power of Gold (3.7): Even if an asset (like gold) has a lower return and higher risk than stocks, it is worth holding if its correlation with the portfolio is low. This shifts the efficient frontier to the left, reducing overall risk.Perfect Negative Correlation (3.8): If two stocks have \(\rho = -1\), a zero-variance portfolio can be created. By setting the weights such that \(w_A\sigma_A = w_B\sigma_B\), we can calculate a synthetic risk-free rate. In this example, that rate is 11.67%.3.9 Abigail Grace Case Study (Portfolio Extension)This problem highlights that adding a risky asset to a portfolio depends on its covariance with existing holdings, not just its standalone risk.Standard Deviation vs. Downside Risk: While standard deviation is the common metric, it treats “good” surprises (upside) the same as “bad” surprises (downside). For investors concerned specifically with capital preservation, Semi-deviation or Value-at-Risk (VaR) are more appropriate measures.3.10 Optimal Active Portfolio ConstructionUsing the Treynor-Black model approach, we analyze individual stocks based on their Alpha (\(\alpha\))—the return earned above what is predicted by the CAPM.StockExpected ReturnBeta (β)Alpha (α)A20%1.3+1.2%B18%1.8-4.4%C17%0.7+3.4%D12%1.0

rf_ch8    <- 8
erm_ch8   <- 16
sigm_ch8  <- 23

stocks <- data.frame(
  stock   = c("A","B","C","D"),
  Er      = c(20, 18, 17, 12),
  beta    = c(1.3, 1.8, 0.7, 1.0),
  sig_eps = c(58, 71, 60, 55),
  stringsAsFactors = FALSE
)

# CAPM required return
stocks$Er_capm   <- rf_ch8 + stocks$beta * (erm_ch8 - rf_ch8)
stocks$alpha     <- stocks$Er - stocks$Er_capm
stocks$excess_Er <- stocks$Er - rf_ch8
stocks$var_eps   <- stocks$sig_eps^2

# Display table — use numeric columns only for rounding
display_df <- data.frame(
  Stock        = stocks$stock,
  Er           = round(stocks$Er,      2),
  Beta         = round(stocks$beta,    2),
  Sig_eps      = round(stocks$sig_eps, 2),
  Er_capm      = round(stocks$Er_capm, 2),
  Alpha        = round(stocks$alpha,   2),
  Excess_Er    = round(stocks$excess_Er, 2),
  Var_eps      = round(stocks$var_eps,   2)
)

kable(display_df,
      caption   = "Stock Characteristics: Excess Returns, Alphas, Residual Variances",
      col.names = c("Stock","E(r)%","Beta","σ(ε)%",
                    "E(r)_CAPM%","Alpha%","Excess E(r)%","σ²(ε)")) %>%
  kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
Stock Characteristics: Excess Returns, Alphas, Residual Variances
Stock E(r)% Beta σ(ε)% E(r)_CAPM% Alpha% Excess E(r)% σ²(ε)
A 20 1.3 58 18.4 1.6 12 3364
B 18 1.8 71 22.4 -4.4 10 5041
C 17 0.7 60 13.6 3.4 9 3600
D 12 1.0 55 16.0 -4.0 4 3025

Part b: Construct the Optimal Risky Portfolio (Treynor-Black)

# Step 1: Initial active portfolio weights (proportional to alpha/residual variance)
stocks$w0       <- stocks$alpha / stocks$var_eps
W_total_raw     <- sum(stocks$w0)
stocks$w_active <- stocks$w0 / W_total_raw

cat("Active portfolio weights:\n")
## Active portfolio weights:
print(data.frame(
  stock    = stocks$stock,
  alpha    = round(stocks$alpha,    4),
  var_eps  = round(stocks$var_eps,  2),
  w_active = round(stocks$w_active, 4)
))
##   stock alpha var_eps w_active
## 1     A   1.6    3364  -0.6136
## 2     B  -4.4    5041   1.1261
## 3     C   3.4    3600  -1.2185
## 4     D  -4.0    3025   1.7060
# Active portfolio statistics
alpha_A  <- sum(stocks$w_active * stocks$alpha)
beta_A   <- sum(stocks$w_active * stocks$beta)
# Residual variance of active portfolio: sum(w_i^2 * sigma_eps_i^2)
var_eA   <- sum(stocks$w_active^2 * stocks$var_eps)   # var_eps already = sig_eps^2
sigma_eA <- sqrt(var_eA)

cat(sprintf("\nActive 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 σ(ε):  %.4f%%\n", sigma_eA))
## Active Portfolio σ(ε):  147.6780%
# Step 2: Initial weight of active portfolio vs passive
# w*_A = [alpha_A / sigma²(eA)] / [E(r_M-rf) / sigma²_M]
var_eA2  <- var_eA   # same quantity, kept as alias for clarity below
w0_A        <- (alpha_A / var_eA2) / ((erm_ch8 - rf_ch8) / sigm_ch8^2)

# Adjustment for beta != 1
w_A_star    <- w0_A / (1 + (1 - beta_A) * w0_A)

cat(sprintf("\nOptimal weight in Active Portfolio (w*_A): %.4f\n", w_A_star))
## 
## Optimal weight in Active Portfolio (w*_A): -0.0486
cat(sprintf("Optimal weight in Passive Portfolio (1-w*_A): %.4f\n", 1 - w_A_star))
## Optimal weight in Passive Portfolio (1-w*_A): 1.0486

Part c: Sharpe Ratio of the Optimal Portfolio

# Sharpe ratio of passive
S_passive <- (erm_ch8 - rf_ch8) / sigm_ch8
cat(sprintf("Sharpe ratio (passive): %.4f\n", S_passive))
## Sharpe ratio (passive): 0.3478
# Information ratio of active portfolio
IR <- alpha_A / sigma_eA
cat(sprintf("Information ratio (active): %.4f\n", IR))
## Information ratio (active): -0.1145
# Sharpe ratio of optimal portfolio
S_optimal <- sqrt(S_passive^2 + IR^2)
cat(sprintf("Sharpe ratio (optimal portfolio): %.4f\n", S_optimal))
## Sharpe ratio (optimal portfolio): 0.3662

Part d: Improvement in Sharpe Ratio

improvement <- S_optimal - S_passive
cat(sprintf("Improvement in Sharpe ratio: %.4f\n", improvement))
## Improvement in Sharpe ratio: 0.0183
cat(sprintf("Passive Sharpe: %.4f → Optimal Sharpe: %.4f (gain: %.4f)\n",
            S_passive, S_optimal, improvement))
## Passive Sharpe: 0.3478 → Optimal Sharpe: 0.3662 (gain: 0.0183)

The active portfolio increases the Sharpe ratio by combining the squared information ratio with the squared Sharpe ratio of the passive market portfolio. This is the key result of the Treynor–Black model, which shows that even small alpha signals, when optimally used, can significantly improve a portfolio’s risk-adjusted returns.

Part e: Complete Portfolio Composition (Risk Aversion A = 2.8)

# Expected return and variance of optimal risky portfolio
Er_opt    <- rf_ch8 + w_A_star * alpha_A +
             (w_A_star * beta_A + (1 - w_A_star)) * (erm_ch8 - rf_ch8)
var_opt   <- (w_A_star * beta_A + (1 - w_A_star))^2 * sigm_ch8^2 +
             w_A_star^2 * var_eA2
sigma_opt <- sqrt(var_opt)

# Optimal allocation to risky portfolio: y* = E(r_p - rf) / (A * sigma²_p)
A_coef  <- 2.8
y_star  <- (Er_opt - rf_ch8) / (A_coef * var_opt)

cat(sprintf("Optimal risky portfolio E(r): %.4f%%\n", Er_opt))
## Optimal risky portfolio E(r): 16.4004%
cat(sprintf("Optimal risky portfolio σ:    %.4f%%\n", sigma_opt))
## Optimal risky portfolio σ:    22.9408%
cat(sprintf("\nFor A = 2.8:\n"))
## 
## For A = 2.8:
cat(sprintf("Allocation to risky portfolio (y*): %.4f (%.2f%%)\n",
            y_star, y_star * 100))
## Allocation to risky portfolio (y*): 0.0057 (0.57%)
cat(sprintf("Allocation to risk-free (1-y*):     %.4f (%.2f%%)\n",
            1 - y_star, (1 - y_star) * 100))
## Allocation to risk-free (1-y*):     0.9943 (99.43%)
cat(sprintf("\nWithin risky portion:\n"))
## 
## Within risky portion:
cat(sprintf("  Active portfolio:  %.2f%%\n", w_A_star * 100))
##   Active portfolio:  -4.86%
cat(sprintf("  Passive portfolio: %.2f%%\n", (1 - w_A_star) * 100))
##   Passive portfolio: 104.86%

3.11 Chapter 8: CFA Problem 1 — Regression-Based Risk Analysis Given (5-year OLS regression of excess stock returns on market excess returns): Statistic ABC XYZ Alpha −3.20% 7.30% Beta 0.60 0.97 R² 0.35 0.17 Residual SD 13.02% 21.45% Recent brokerage beta estimates (2-year weekly): Brokerage ABC Beta XYZ Beta A 0.62 1.45 B 0.71 1.25 Interpretation ABC Stock: An alpha of −3.20% indicates that ABC has historically underperformed relative to its CAPM-predicted return, implying negative abnormal performance over the sample period. A beta of 0.60 suggests relatively low sensitivity to market movements, making ABC more defensive in nature. An R² of 0.35 means that 35% of return variation is explained by market exposure, while the remaining 65% is driven by firm-specific factors. The relatively high residual standard deviation (13.02%) indicates substantial idiosyncratic risk despite the low beta. From a forward-looking perspective, a negative historical alpha does not necessarily imply continued underperformance, but it does warrant further investigation into whether the firm’s fundamentals have shifted. Brokerage beta estimates (0.62 and 0.71) are close to the historical estimate of 0.60, suggesting beta stability and supporting its use as a reasonable forecast. In a diversified portfolio, the large firm-specific risk is largely diversified away. XYZ Stock: A positive alpha of 7.30% indicates strong historical outperformance relative to CAPM expectations. A beta of 0.97 implies near-market sensitivity, with returns closely tracking overall market movements. An R² of 0.17 suggests that only a small portion of return variation is explained by the market, with most risk being idiosyncratic. The high residual standard deviation (21.45%) reflects substantial firm-specific volatility. The large positive alpha may reflect genuine skill or could be noise, especially given the low explanatory power of the model. Brokerage beta estimates (1.25–1.45) differ significantly from the historical beta of 0.97, indicating potential instability in risk exposure. This raises concern that XYZ may have become more sensitive to market movements in recent periods, meaning historical estimates may understate current risk. Portfolio Implications In well-diversified portfolios, idiosyncratic risk is largely eliminated, making beta and alpha the primary relevant metrics. ABC’s negative alpha combined with stable beta suggests it may be less attractive on a risk-adjusted basis. XYZ’s strong alpha must be weighed against its unstable beta and high uncertainty in risk estimates. The upward revision in XYZ’s beta is particularly important, as it implies materially higher systematic risk than historical estimates suggest. 4 Discussion The empirical findings from Part I yield several key insights: CAPM vs FF3: The two frameworks produce different MVP allocations because they capture covariance differently. CAPM relies on a single market factor, which can understate co-movement among assets with shared style exposures. In contrast, the FF3 model incorporates additional factors, leading to a more detailed covariance structure and typically a more conservatively diversified minimum variance portfolio. MVP in Practice: The minimum variance portfolio focuses solely on risk minimization and does not incorporate expected returns. This makes it less sensitive to forecasting errors in mean returns but can result in portfolios with unintuitively low expected performance. Practitioners often adjust this by incorporating return targets or Bayesian priors. Rolling Window Estimation: A 60-month rolling window offers a practical balance between stability and responsiveness. Shorter windows increase estimation noise, while longer windows risk using outdated information. The shift between March and April 2025 illustrates how portfolio weights adjust as new observations replace older data. 5 Conclusion This midterm project demonstrates the full pipeline of modern portfolio analysis, from data preparation and empirical estimation to theoretical interpretation. Key conclusions include: CAPM provides a useful baseline but may underestimate covariance among assets with shared factor exposures. FF3 improves this by incorporating additional systematic risk dimensions. The MVP approach is powerful because it does not require expected return forecasts and emphasizes diversification across low-correlation assets, though it may generate extreme allocations. Split-sample evidence suggests that return distributions are not stable over time, highlighting the importance of caution when extrapolating historical statistics. Core theory reinforced through textbook problems includes utility maximization, the Capital Market Line, diversification mechanics, and the Treynor–Black framework. A central takeaway is that portfolio construction is driven primarily by correlation structure rather than individual asset characteristics. Understanding this, along with the limitations of any model, is essential for building robust and resilient portfolios.

This document represents a complete midterm submission. All results are reproducible using the provided datasets, and the analysis has been published to RPubs for assessment.