{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE)

R Markdown

ETF Portfolio Analysis with CAPM and FF 3-Factor Model

This script performs portfolio analysis on 8 ETFs using different factor models

1. Import data

library(tidyquant) library(lubridate) library(timetk) library(dplyr) library(purrr) library(PerformanceAnalytics) library(quadprog)

Define ETF tickers

etf_tickers <- c(“SPY”, “QQQ”, “EEM”, “IWM”, “EFA”, “TLT”, “VNQ”, “GLD”) # Note: IYR changed to VNQ which is the larger Vanguard REIT ETF

Download daily data from 2010 to current date

etf_daily <- tq_get(etf_tickers, from = “2010-01-01”, to = Sys.Date(), get = “stock.prices”)

Check the first few rows of the data

head(etf_daily)

Use adjusted prices for calculations

etf_daily_adj <- etf_daily %>% select(symbol, date, adjusted)

Reshape data to wide format for easier calculations

etf_daily_wide <- etf_daily_adj %>% pivot_wider(names_from = symbol, values_from = adjusted)

Display the first few rows of the wide data

head(etf_daily_wide)

2. Calculate weekly and monthly returns

Calculate weekly returns

etf_weekly_returns <- etf_daily_adj %>% group_by(symbol) %>% tq_transmute(select = adjusted, mutate_fun = periodReturn, period = “weekly”, col_rename = “weekly_return”)

Reshape weekly returns to wide format

etf_weekly_returns_wide <- etf_weekly_returns %>% pivot_wider(names_from = symbol, values_from = weekly_return)

Calculate monthly returns

etf_monthly_returns <- etf_daily_adj %>% group_by(symbol) %>% tq_transmute(select = adjusted, mutate_fun = periodReturn, period = “monthly”, col_rename = “monthly_return”)

Reshape monthly returns to wide format

etf_monthly_returns_wide <- etf_monthly_returns %>% pivot_wider(names_from = symbol, values_from = monthly_return)

Display the first few rows of monthly returns

head(etf_monthly_returns_wide)

3. Download and process Fama-French 3-factor data

The direct URL for the FF factors data (monthly)

ff_url <- “https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip

Create a temporary file to download the zip file

temp_file <- tempfile() download.file(ff_url, temp_file)

Unzip and read the CSV file

ff_files <- unzip(temp_file, list = TRUE) ff_data_file <- unzip(temp_file, files = ff_files$Name[1], exdir = tempdir())

Read the data with more flexible approach to handle column naming

ff_data_raw <- read.csv(ff_data_file, skip = 3, header = FALSE)

Check the column names or first few rows to understand the structure

print(head(ff_data_raw))

Process the data - adjust column names based on actual structure

Assuming the structure is: Date, Mkt-RF, SMB, HML, RF

colnames(ff_data_raw) <- c(“date”, “Mkt_RF”, “SMB”, “HML”, “RF”)

ff_data <- ff_data_raw %>% filter(date != ““) %>% mutate(across(c(Mkt_RF, SMB, HML, RF), as.numeric)) %>% filter(!is.na(Mkt_RF))

Convert percentage values to decimal

ff_data <- ff_data %>% mutate(across(c(Mkt_RF, SMB, HML, RF), ~ . / 100))

Create proper date format - assuming date is in YYYYMM format

ff_data <- ff_data %>% mutate( year = as.integer(substr(date, 1, 4)), month = as.integer(substr(date, 5, 6)), date = ymd(paste(year, month, “01”, sep = “-”)) ) %>% select(-year, -month)

Keep only data from 2010 onwards

ff_data <- ff_data %>% filter(date >= “2010-01-01”)

Display the first few rows of FF data

head(ff_data)

4. Merge monthly return data from questions 2 and 3

Prepare ETF monthly returns for merging

etf_monthly_returns_for_merge <- etf_monthly_returns_wide %>% mutate(date = floor_date(date, “month”))

Merge ETF returns with FF factors

merged_data <- left_join(etf_monthly_returns_for_merge, ff_data, by = “date”) head(merged_data)

5. Compute MVP based on CAPM model using past 60-month returns

Filter data for the required 60-month period

capm_period_data <- merged_data %>% filter(date >= “2019-03-01” & date <= “2024-02-29”)

Create excess returns for each ETF

capm_period_data <- capm_period_data %>% mutate(across(all_of(etf_tickers), ~ . - RF, .names = “{col}_excess”))

Prepare data for CAPM regression

excess_returns_matrix <- as.matrix(capm_period_data %>% select(ends_with(“_excess”))) market_excess_returns <- capm_period_data$Mkt_RF

Function to estimate CAPM parameters for an asset

estimate_capm <- function(excess_returns, market_excess) { model <- lm(excess_returns ~ market_excess) beta <- coef(model)[2] alpha <- coef(model)[1] residuals <- residuals(model) list(beta = beta, alpha = alpha, residuals = residuals) }

Estimate CAPM parameters for each ETF

capm_params <- list() for (i in 1:length(etf_tickers)) { capm_params[[etf_tickers[i]]] <- estimate_capm(excess_returns_matrix[, i], market_excess_returns) }

Calculate covariance matrix based on CAPM

sigma_market <- var(market_excess_returns) n_assets <- length(etf_tickers) capm_cov_matrix <- matrix(0, n_assets, n_assets)

for (i in 1:n_assets) { for (j in 1:n_assets) { if (i == j) { # Diagonal: beta^2 * sigma_market^2 + var(residuals) capm_cov_matrix[i, i] <- (capm_params[[etf_tickers[i]]]\(beta)^2 * sigma_market + var(capm_params[[etf_tickers[i]]]\)residuals) } else { # Off-diagonal: beta_i * beta_j * sigma_market^2 capm_cov_matrix[i, j] <- capm_params[[etf_tickers[i]]]\(beta * capm_params[[etf_tickers[j]]]\)beta * sigma_market } } }

Name rows and columns of the covariance matrix

rownames(capm_cov_matrix) <- etf_tickers colnames(capm_cov_matrix) <- etf_tickers

Compute MVP weights based on CAPM covariance matrix

Dmat <- capm_cov_matrix dvec <- rep(0, n_assets) Amat <- cbind(rep(1, n_assets), diag(n_assets)) bvec <- c(1, rep(0, n_assets))

capm_mvp_solution <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1) capm_mvp_weights <- capm_mvp_solution$solution names(capm_mvp_weights) <- etf_tickers

Display CAPM-based MVP weights

capm_mvp_weights

6. Compute MVP based on FF 3-factor model

Prepare data for FF model regression

ff_factors <- as.matrix(capm_period_data %>% select(Mkt_RF, SMB, HML))

Function to estimate FF 3-factor parameters for an asset

estimate_ff3 <- function(excess_returns, factors) { model <- lm(excess_returns ~ factors) betas <- coef(model)[-1] # Exclude intercept alpha <- coef(model)[1] residuals <- residuals(model) list(betas = betas, alpha = alpha, residuals = residuals) }

Estimate FF 3-factor parameters for each ETF

ff3_params <- list() for (i in 1:length(etf_tickers)) { ff3_params[[etf_tickers[i]]] <- estimate_ff3(excess_returns_matrix[, i], ff_factors) }

Calculate factor covariance matrix

factor_cov_matrix <- cov(ff_factors)

Calculate covariance matrix based on FF 3-factor model

ff3_cov_matrix <- matrix(0, n_assets, n_assets)

for (i in 1:n_assets) { for (j in 1:n_assets) { if (i == j) { # Diagonal: beta’ * factor_cov * beta + var(residuals) ff3_cov_matrix[i, i] <- t(ff3_params[[etf_tickers[i]]]\(betas) %*% factor_cov_matrix %*% ff3_params[[etf_tickers[i]]]\)betas + var(ff3_params[[etf_tickers[i]]]\(residuals) } else { # Off-diagonal: beta_i' * factor_cov * beta_j ff3_cov_matrix[i, j] <- t(ff3_params[[etf_tickers[i]]]\)betas) %% factor_cov_matrix %% ff3_params[[etf_tickers[j]]]$betas } } }

Name rows and columns of the covariance matrix

rownames(ff3_cov_matrix) <- etf_tickers colnames(ff3_cov_matrix) <- etf_tickers

Compute MVP weights based on FF 3-factor covariance matrix

Dmat <- ff3_cov_matrix dvec <- rep(0, n_assets) Amat <- cbind(rep(1, n_assets), diag(n_assets)) bvec <- c(1, rep(0, n_assets))

ff3_mvp_solution <- solve.QP(Dmat, dvec, Amat, bvec, meq = 1) ff3_mvp_weights <- ff3_mvp_solution$solution names(ff3_mvp_weights) <- etf_tickers

Display FF 3-factor based MVP weights

ff3_mvp_weights

7. Calculate realized portfolio returns for March 2024

Get March 2024 returns

march_2024_returns <- merged_data %>% filter(format(date, “%Y-%m”) == “2024-03”) %>% select(all_of(etf_tickers))

If there are multiple rows for March 2024, take the one with the latest date

if (nrow(march_2024_returns) > 1) { march_2024_returns <- march_2024_returns %>% slice_tail(n = 1) }

Calculate realized returns using CAPM-based weights

capm_realized_return <- sum(as.numeric(march_2024_returns) * capm_mvp_weights)

Calculate realized returns using FF 3-factor based weights

ff3_realized_return <- sum(as.numeric(march_2024_returns) * ff3_mvp_weights)

Display results

cat(“CAPM-based MVP weights:”) print(capm_mvp_weights)

cat(“-factor-based MVP weights:”) print(ff3_mvp_weights)

cat(“portfolio return in March 2024 using CAPM-based MVP weights:”, capm_realized_return, “”)

cat(“Realized portfolio return in March 2024 using FF 3-factor-based MVP weights:”, ff3_realized_return, “”)