{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE)
library(tidyquant) library(lubridate) library(timetk) library(dplyr) library(purrr) library(PerformanceAnalytics) library(quadprog)
etf_tickers <- c(“SPY”, “QQQ”, “EEM”, “IWM”, “EFA”, “TLT”, “VNQ”, “GLD”) # Note: IYR changed to VNQ which is the larger Vanguard REIT ETF
etf_daily <- tq_get(etf_tickers, from = “2010-01-01”, to = Sys.Date(), get = “stock.prices”)
head(etf_daily)
etf_daily_adj <- etf_daily %>% select(symbol, date, adjusted)
etf_daily_wide <- etf_daily_adj %>% pivot_wider(names_from = symbol, values_from = adjusted)
head(etf_daily_wide)
etf_weekly_returns <- etf_daily_adj %>% group_by(symbol) %>% tq_transmute(select = adjusted, mutate_fun = periodReturn, period = “weekly”, col_rename = “weekly_return”)
etf_weekly_returns_wide <- etf_weekly_returns %>% pivot_wider(names_from = symbol, values_from = weekly_return)
etf_monthly_returns <- etf_daily_adj %>% group_by(symbol) %>% tq_transmute(select = adjusted, mutate_fun = periodReturn, period = “monthly”, col_rename = “monthly_return”)
etf_monthly_returns_wide <- etf_monthly_returns %>% pivot_wider(names_from = symbol, values_from = monthly_return)
head(etf_monthly_returns_wide)
ff_url <- “https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip”
temp_file <- tempfile() download.file(ff_url, temp_file)
ff_files <- unzip(temp_file, list = TRUE) ff_data_file <- unzip(temp_file, files = ff_files$Name[1], exdir = tempdir())
ff_data_raw <- read.csv(ff_data_file, skip = 3, header = FALSE)
print(head(ff_data_raw))
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))
ff_data <- ff_data %>% mutate(across(c(Mkt_RF, SMB, HML, RF), ~ . / 100))
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)
ff_data <- ff_data %>% filter(date >= “2010-01-01”)
head(ff_data)
etf_monthly_returns_for_merge <- etf_monthly_returns_wide %>% mutate(date = floor_date(date, “month”))
merged_data <- left_join(etf_monthly_returns_for_merge, ff_data, by = “date”) head(merged_data)
capm_period_data <- merged_data %>% filter(date >= “2019-03-01” & date <= “2024-02-29”)
capm_period_data <- capm_period_data %>% mutate(across(all_of(etf_tickers), ~ . - RF, .names = “{col}_excess”))
excess_returns_matrix <- as.matrix(capm_period_data %>% select(ends_with(“_excess”))) market_excess_returns <- capm_period_data$Mkt_RF
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) }
capm_params <- list() for (i in 1:length(etf_tickers)) { capm_params[[etf_tickers[i]]] <- estimate_capm(excess_returns_matrix[, i], market_excess_returns) }
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 } } }
rownames(capm_cov_matrix) <- etf_tickers colnames(capm_cov_matrix) <- etf_tickers
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
capm_mvp_weights
ff_factors <- as.matrix(capm_period_data %>% select(Mkt_RF, SMB, HML))
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) }
ff3_params <- list() for (i in 1:length(etf_tickers)) { ff3_params[[etf_tickers[i]]] <- estimate_ff3(excess_returns_matrix[, i], ff_factors) }
factor_cov_matrix <- cov(ff_factors)
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 } } }
rownames(ff3_cov_matrix) <- etf_tickers colnames(ff3_cov_matrix) <- etf_tickers
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
ff3_mvp_weights
march_2024_returns <- merged_data %>% filter(format(date, “%Y-%m”) == “2024-03”) %>% select(all_of(etf_tickers))
if (nrow(march_2024_returns) > 1) { march_2024_returns <- march_2024_returns %>% slice_tail(n = 1) }
capm_realized_return <- sum(as.numeric(march_2024_returns) * capm_mvp_weights)
ff3_realized_return <- sum(as.numeric(march_2024_returns) * ff3_mvp_weights)
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, “”)