# Load necessary libraries
library(tidyquant) 
## Loading required package: lubridate
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
## Loading required package: PerformanceAnalytics
## Loading required package: xts
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## 
## Attaching package: 'PerformanceAnalytics'
## The following object is masked from 'package:graphics':
## 
##     legend
## Loading required package: quantmod
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
##   method            from
##   as.zoo.data.frame zoo
library(lubridate)
library(timetk)
library(purrr)
library(quantmod)
library(dplyr)
## 
## ######################### Warning from 'xts' package ##########################
## #                                                                             #
## # The dplyr lag() function breaks how base R's lag() function is supposed to  #
## # work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or       #
## # source() into this session won't work correctly.                            #
## #                                                                             #
## # Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
## # conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop           #
## # dplyr from breaking base R's lag() function.                                #
## #                                                                             #
## # Code in packages is not affected. It's protected by R's namespace mechanism #
## # Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning.  #
## #                                                                             #
## ###############################################################################
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:xts':
## 
##     first, last
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)

# Define the ETF symbols
symbols <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")

# Download the data from Yahoo Finance
prices <- getSymbols(symbols, src = 'yahoo', from = "2010-01-01", auto.assign = TRUE, warnings = FALSE) %>% 
  map(~ Ad(get(.))) %>% 
  reduce(merge) %>%
  `colnames<-`(symbols)

# Convert xts to tibble
prices_tibble <- tk_tbl(prices, rename_index = "date")

# Ensure date column is in character format, then convert to Date type
prices_tibble <- prices_tibble %>%
  mutate(date = as.character(date)) %>%
  mutate(date = as.Date(date))

# Calculate weekly log returns
weekly_prices <- prices_tibble %>%
  group_by(week = floor_date(date, "week")) %>%
  summarize(across(all_of(symbols), last))

weekly_returns <- weekly_prices %>%
  mutate(across(all_of(symbols), ~ log(. / lag(.)))) %>%
  drop_na()

# Calculate monthly log returns
monthly_prices <- prices_tibble %>%
  group_by(month = floor_date(date, "month")) %>%
  summarize(across(all_of(symbols), last))
monthly_returns <- monthly_prices %>%
  mutate(across(all_of(symbols), ~ log(. / lag(.)))) %>%
  drop_na()

# Convert to tibble format
monthly_returns_tibble <- as_tibble(monthly_returns)

# Rename 'month' column to 'date'
monthly_returns_tibble <- monthly_returns_tibble %>%
  rename(date = month)

# Load the Fama/French data from URL
FFdata <- read.csv("https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors.CSV", skip = 3)

# Clean and format the data, handling parsing errors
FFdata <- FFdata %>%
  mutate(date = parse_date_time(X, orders = "Y%m", quiet = TRUE)) %>%
  filter(!is.na(date)) %>%
  select(date, Mkt.RF, SMB, HML, RF) 

# Convert character columns to numeric
FFdata <- FFdata %>%
  mutate(across(-date, ~ as.numeric(gsub(" ", "", .))))
# Ensure date columns are present and in Date format
FFdata <- FFdata %>%
  mutate(date = as.Date(date))

# Merge the ETF returns with the FF factors data
merged_data <- left_join(monthly_returns_tibble, FFdata, by = 'date')

# Extract the relevant returns for the CAPM model
capm_returns <- merged_data %>%
  filter(date >= "2010-02-01" & date <= "2015-01-31") %>%
  select(-date, -Mkt.RF, -SMB, -HML, -RF)

# Compute the covariance matrix
capm_cov_matrix <- cov(capm_returns)

# Extract the relevant returns and FF factors for the FF model
ff_returns <- merged_data %>%
  filter(date >= "2010-02-01" & date <= "2015-01-31")

# Compute the residuals for each ETF by regressing on the FF factors
residuals <- ff_returns %>%
  select(all_of(symbols)) %>%
  map_dfc(~ residuals(lm(. ~ Mkt.RF + SMB + HML, data = ff_returns)))

# Compute the covariance matrix of the residuals
ff_cov_matrix <- cov(residuals)

# Function to compute GMV portfolio weights
compute_gmv_weights <- function(cov_matrix) {
  inv_cov <- solve(cov_matrix)
  ones <- rep(1, nrow(inv_cov))
  weights <- inv_cov %*% ones / sum(inv_cov %*% ones)
  return(weights)
}

# Compute the weights
gmv_weights_capm <- compute_gmv_weights(capm_cov_matrix)
gmv_weights_ff <- compute_gmv_weights(ff_cov_matrix)

# Print weights for verification
print(gmv_weights_capm)
##            [,1]
## SPY  0.94116583
## QQQ -0.18088331
## EEM -0.01437082
## IWM -0.05252841
## EFA -0.01342445
## TLT  0.48106300
## IYR -0.21173959
## GLD  0.05071774
print(gmv_weights_ff)
##              [,1]
## SPY  0.8299846916
## QQQ  0.0052295955
## EEM  0.0001153044
## IWM  0.1868628046
## EFA  0.0009328018
## TLT  0.0032729219
## IYR -0.0286049238
## GLD  0.0022068040
# Function to compute cumulative returns from weights and returns
compute_cumulative_returns <- function(returns, weights) {
  portfolio_returns <- rowSums(returns * weights)
  cumulative_returns <- cumprod(1 + portfolio_returns) - 1
  return(cumulative_returns)
}

# Prepare returns data for backtesting
returns_data <- merged_data %>% 
  filter(date > as.Date("2015-01-31")) %>%
  select(date, all_of(symbols)) %>%
  mutate(across(where(is.numeric), ~exp(.) - 1)) %>%  # Convert log returns back to normal returns
  tk_tbl(rename_index = "date")
## Warning in tk_tbl.data.frame(., rename_index = "date"): Warning: No index to
## preserve. Object otherwise converted to tibble successfully.
# Backtest using CAPM model
cum_returns_capm <- compute_cumulative_returns(returns_data %>% select(-date), gmv_weights_capm)

# Backtest using Fama-French model
cum_returns_ff <- compute_cumulative_returns(returns_data %>% select(-date), gmv_weights_ff)

# Plot cumulative returns
plot(as.Date(returns_data$date), cum_returns_capm, type = "l", col = "blue", xlab = "Time", ylab = "Cumulative Returns", main = "GMV Portfolio Performance")
lines(as.Date(returns_data$date), cum_returns_ff, col = "red")
legend("topright", legend = c("CAPM", "Fama-French"), col = c("blue", "red"), lty = 1)