Part 1.
Chapter 6 Problem set 21
# Given data
E_rP <- 0.11 # Expected return of risky portfolio
sigma_P <- 0.15 # Standard deviation of risky portfolio
r_f <- 0.05 # Risk-free rate
# Part (a): Find proportion to invest in risky portfolio (y)
target_return <- 0.08
y_client1 <- (target_return - r_f) / (E_rP - r_f)
y_client1
## [1] 0.5
x_client1 <- 1 - y_client1 # Proportion in risk-free asset
x_client1
## [1] 0.5
# Part (b): Find standard deviation of client's portfolio
sigma_client1 <- y_client1 * sigma_P
sigma_client1
## [1] 0.075
# Part (c): Find proportion to invest for second client
sigma_target <- 0.12
y_client2 <- sigma_target / sigma_P
y_client2
## [1] 0.8
E_r_client2 <- r_f + y_client2 * (E_rP - r_f)
E_r_client2
## [1] 0.098
# Compare risk aversion
risk_aversion_client1 <- (E_rP - r_f) / sigma_P / y_client1
risk_aversion_client2 <- (E_rP - r_f) / sigma_P / y_client2
risk_aversion_client1
## [1] 0.8
risk_aversion_client2
## [1] 0.5
# Higher risk aversion means more conservative investment
more_risk_averse <- ifelse(risk_aversion_client1 > risk_aversion_client2, "Client 1", "Client 2")
more_risk_averse
## [1] "Client 1"
Chapter 6 Problem set 22
# Given data
E_rM <- 0.12 # Expected return on market portfolio
sigma_M <- 0.20 # Standard deviation of market portfolio
r_f <- 0.05 # Risk-free rate
sigma_C <- 0.10 # Desired standard deviation of portfolio
# Compute proportion invested in market
y <- sigma_C / sigma_M
y
## [1] 0.5
# Compute expected return using Capital Market Line (CML)
E_rC <- r_f + y * (E_rM - r_f)
E_rC
## [1] 0.085
Chapter 6 CFA Problem 8
# Given data
w_E <- 0.6 # Weight in equity fund
w_F <- 0.4 # Weight in T-bills
risk_premium <- 0.10 # 10%
rf <- 0.06 # Risk-free rate
sd_equity <- 0.14 # Standard deviation of equity fund
# Expected return calculation
expected_return_equity <- rf + risk_premium
expected_return_portfolio <- w_E * expected_return_equity + w_F * rf
# Standard deviation calculation (T-bills have zero risk)
sd_portfolio <- w_E * sd_equity
# Output results
cat("Expected Return of Portfolio:", expected_return_portfolio * 100, "%\n")
## Expected Return of Portfolio: 12 %
cat("Standard Deviation of Portfolio:", sd_portfolio * 100, "%\n")
## Standard Deviation of Portfolio: 8.4 %
Chapter 7 Problem set 11
# Load necessary libraries
library(ggplot2)
# Given data
mu_stock <- 0.18 # Expected return of stocks
sd_stock <- 0.22 # Standard deviation of stocks
mu_gold <- 0.10 # Expected return of gold
sd_gold <- 0.30 # Standard deviation of gold
rho <- -0.2 # Assumed correlation between stocks and gold
# Covariance calculation
cov_stock_gold <- rho * sd_stock * sd_gold
# Generate portfolio weights
w <- seq(0, 1, by = 0.01) # Portfolio weights for stocks
# Compute portfolio return and standard deviation
mu_portfolio <- w * mu_stock + (1 - w) * mu_gold
sd_portfolio <- sqrt((w^2 * sd_stock^2) + ((1 - w)^2 * sd_gold^2) +
(2 * w * (1 - w) * cov_stock_gold))
# Plot efficient frontier
df <- data.frame(Return = mu_portfolio, Risk = sd_portfolio)
ggplot(df, aes(x = Risk, y = Return)) +
geom_line(color = "blue", size = 1) +
geom_point(aes(x = sd_stock, y = mu_stock), color = "red", size = 3, label="Stocks") +
geom_point(aes(x = sd_gold, y = mu_gold), color = "gold", size = 3, label="Gold") +
labs(title = "Efficient Frontier: Stocks and Gold", x = "Standard Deviation (Risk)", y = "Expected Return") +
theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning in geom_point(aes(x = sd_stock, y = mu_stock), color = "red", size = 3,
## : Ignoring unknown parameters: `label`
## Warning in geom_point(aes(x = sd_gold, y = mu_gold), color = "gold", size = 3,
## : Ignoring unknown parameters: `label`
## Warning in geom_point(aes(x = sd_stock, y = mu_stock), color = "red", size = 3, : All aesthetics have length 1, but the data has 101 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
## Warning in geom_point(aes(x = sd_gold, y = mu_gold), color = "gold", size = 3, : All aesthetics have length 1, but the data has 101 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
# Case (b): Perfect Correlation (ρ = 1)
rho_perfect <- 1
cov_stock_gold_perfect <- rho_perfect * sd_stock * sd_gold
# Compute portfolio risk with perfect correlation
sd_portfolio_perfect <- w * sd_stock + (1 - w) * sd_gold
df_perfect <- data.frame(Return = mu_portfolio, Risk = sd_portfolio_perfect)
ggplot(df_perfect, aes(x = Risk, y = Return)) +
geom_line(color = "red", size = 1) +
geom_point(aes(x = sd_stock, y = mu_stock), color = "blue", size = 3) +
geom_point(aes(x = sd_gold, y = mu_gold), color = "gold", size = 3) +
labs(title = "Portfolio with Perfect Correlation (ρ = 1)", x = "Standard Deviation (Risk)", y = "Expected Return") +
theme_minimal()
## Warning in geom_point(aes(x = sd_stock, y = mu_stock), color = "blue", size = 3): All aesthetics have length 1, but the data has 101 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
## Warning in geom_point(aes(x = sd_gold, y = mu_gold), color = "gold", size = 3): All aesthetics have length 1, but the data has 101 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
#Part (a): Even though gold has a lower return and higher volatility, it can still be useful in a diversified portfolio if it has a low or negative correlation with stocks. This is illustrated in the first plot, where portfolios including gold may have lower overall risk.
#Part (b): If gold and stocks have a perfect correlation (ρ = 1), adding gold does not improve diversification. The portfolio’s risk is a weighted average of the individual risks, and the efficient frontier is simply a straight line between the two assets. This is shown in the second plot.
#Part (c): If gold is inferior in both return and volatility and has perfect correlation with stocks, it should not be held in equilibrium. Investors would prefer stocks, which dominate gold in all aspects. In a real market, such a situation would not persist since gold would be priced lower until its expected return adjusts to justify its risk.
Chapter 7 Problem set 12
# Given data
mu_A <- 0.10 # Expected return of Stock A
sigma_A <- 0.15 # Standard deviation of Stock A
mu_B <- 0.05 # Expected return of Stock B
sigma_B <- 0.10 # Standard deviation of Stock B
rho_AB <- -1 # Perfect negative correlation
# Solve for weights
w_B <- 3/5 # Derived from solving w_A * sigma_A = w_B * sigma_B
w_A <- 2/5
# Compute risk-free rate
rf <- w_A * mu_A + w_B * mu_B
print(paste("The risk-free rate is:", round(rf * 100, 2), "%"))
## [1] "The risk-free rate is: 7 %"
Chapter 7 CFA Problem 1-3
# Load required library
library(ggplot2)
# Define function for portfolio risk based on number of stocks
portfolio_risk <- function(n, market_risk = 0.20, unsystematic_risk = 0.30) {
# Portfolio risk formula: Diversification effect
total_risk <- sqrt((market_risk^2) + (unsystematic_risk^2 / n))
return(total_risk)
}
# Simulate risk changes for different portfolio sizes
num_stocks <- seq(5, 50, by = 1)
risks <- sapply(num_stocks, portfolio_risk)
# Plot portfolio risk vs number of stocks
df <- data.frame(Stocks = num_stocks, Risk = risks)
ggplot(df, aes(x = Stocks, y = Risk)) +
geom_line(color = "blue", size = 1) +
geom_vline(xintercept = 40, linetype = "dashed", color = "red") +
geom_vline(xintercept = 20, linetype = "dashed", color = "green") +
geom_vline(xintercept = 10, linetype = "dashed", color = "purple") +
labs(title = "Portfolio Risk vs. Number of Stocks",
x = "Number of Stocks", y = "Portfolio Standard Deviation") +
theme_minimal()
#1a: Reducing to 20 increases risk but remains manageable.
#1b: If stocks are highly correlated, the risk impact is smaller.
#2: Reducing to 10 increases risk significantly.
#3: A broader view should be taken, as Hennessy’s portfolio is only part of the total Wilstead fund.
Chapter 8 Problem set 17
# Given data
stocks <- data.frame(
Asset = c("A", "B", "C", "D"),
Expected_Return = c(20, 18, 17, 12), # In percentage
Beta = c(1.3, 1.8, 0.7, 1.0),
Residual_SD = c(58, 71, 60, 55) # In percentage
)
risk_free_rate <- 8 # T-bill return (%)
market_return <- 16 # Passive portfolio return (%)
market_sd <- 23 # Passive portfolio standard deviation (%)
# Compute expected excess returns
stocks$Excess_Return <- stocks$Expected_Return - risk_free_rate
# Compute alpha values
market_risk_premium <- market_return - risk_free_rate
stocks$Alpha <- stocks$Expected_Return - (risk_free_rate + stocks$Beta * market_risk_premium)
# Compute residual variances
stocks$Residual_Variance <- (stocks$Residual_SD / 100)^2 # Convert to variance
print(stocks)
## Asset Expected_Return Beta Residual_SD Excess_Return Alpha Residual_Variance
## 1 A 20 1.3 58 12 1.6 0.3364
## 2 B 18 1.8 71 10 -4.4 0.5041
## 3 C 17 0.7 60 9 3.4 0.3600
## 4 D 12 1.0 55 4 -4.0 0.3025
# Compute weights for the optimal risky portfolio
stocks$Weight <- stocks$Alpha / stocks$Residual_Variance
sum_weights <- sum(stocks$Weight)
stocks$Weight <- stocks$Weight / sum_weights # Normalize
# Compute tracking error variance
theta <- sum(stocks$Weight^2 * stocks$Residual_Variance)
print(stocks)
## Asset Expected_Return Beta Residual_SD Excess_Return Alpha Residual_Variance
## 1 A 20 1.3 58 12 1.6 0.3364
## 2 B 18 1.8 71 10 -4.4 0.5041
## 3 C 17 0.7 60 9 3.4 0.3600
## 4 D 12 1.0 55 4 -4.0 0.3025
## Weight
## 1 -0.613639
## 2 1.126121
## 3 -1.218500
## 4 1.706018
print(paste("Tracking Error Variance (Theta):", round(theta, 4)))
## [1] "Tracking Error Variance (Theta): 2.1809"
# Compute alpha_P
alpha_P <- sum(stocks$Weight * stocks$Alpha)
# Compute Sharpe Ratio of the optimal portfolio
Sharpe_optimal <- alpha_P / sqrt(theta)
print(paste("Sharpe Ratio of the Optimal Portfolio:", round(Sharpe_optimal, 4)))
## [1] "Sharpe Ratio of the Optimal Portfolio: -11.4463"
# Compute Sharpe Ratio of the passive portfolio
Sharpe_market <- (market_return - risk_free_rate) / market_sd
# Compute improvement in Sharpe Ratio
Sharpe_improvement <- sqrt(Sharpe_market^2 + Sharpe_optimal^2) - Sharpe_market
print(paste("Improvement in Sharpe Ratio:", round(Sharpe_improvement, 4)))
## [1] "Improvement in Sharpe Ratio: 11.1038"
A <- 2.8 # Investor's risk aversion
y_optimal <- Sharpe_optimal / (A * market_sd^2)
print(paste("Proportion Invested in the Optimal Risky Portfolio:", round(y_optimal, 4)))
## [1] "Proportion Invested in the Optimal Risky Portfolio: -0.0077"
print(paste("Proportion Invested in Risk-Free Asset:", round(1 - y_optimal, 4)))
## [1] "Proportion Invested in Risk-Free Asset: 1.0077"
Chapter 8 CFA Problem 1
# Given regression results
abc_alpha <- -0.032 # Alpha (Annualized)
abc_beta <- 0.60 # Beta
abc_r2 <- 0.35 # R-squared
abc_residual_sd <- 0.1302 # Residual standard deviation
xyz_alpha <- 0.073 # Alpha (Annualized)
xyz_beta <- 0.97 # Beta
xyz_r2 <- 0.17 # R-squared
xyz_residual_sd <- 0.2145 # Residual standard deviation
# Additional beta estimates from brokerage houses
abc_beta_a <- 0.62
abc_beta_b <- 0.71
xyz_beta_a <- 1.45
xyz_beta_b <- 1.25
# Average beta estimates for ABC and XYZ
abc_beta_avg <- mean(c(abc_beta_a, abc_beta_b))
xyz_beta_avg <- mean(c(xyz_beta_a, xyz_beta_b))
# Interpretation
interpretation <- function(alpha, beta, r2, residual_sd, stock_name) {
cat("\nStock:", stock_name, "\n")
cat("Alpha (Intercept):", alpha * 100, "% (Annualized)\n")
cat("Beta (Systematic Risk):", beta, "\n")
cat("R-squared (Explained Variation):", r2, "\n")
cat("Residual Standard Deviation (Unsystematic Risk):", residual_sd * 100, "%\n")
if (alpha > 0) {
cat("This stock has generated positive excess returns after adjusting for market risk.\n")
} else {
cat("This stock has underperformed after adjusting for market risk.\n")
}
if (r2 < 0.3) {
cat("The market index explains a small portion of this stock's returns, meaning other factors are influencing its performance.\n")
} else {
cat("A moderate portion of the stock's returns is explained by market movements.\n")
}
if (beta > 1) {
cat("The stock is more volatile than the market and contributes to portfolio risk.\n")
} else if (beta < 1) {
cat("The stock is less volatile than the market, making it a defensive investment.\n")
}
}
# Output interpretations
interpretation(abc_alpha, abc_beta, abc_r2, abc_residual_sd, "ABC")
##
## Stock: ABC
## Alpha (Intercept): -3.2 % (Annualized)
## Beta (Systematic Risk): 0.6
## R-squared (Explained Variation): 0.35
## Residual Standard Deviation (Unsystematic Risk): 13.02 %
## This stock has underperformed after adjusting for market risk.
## A moderate portion of the stock's returns is explained by market movements.
## The stock is less volatile than the market, making it a defensive investment.
interpretation(xyz_alpha, xyz_beta, xyz_r2, xyz_residual_sd, "XYZ")
##
## Stock: XYZ
## Alpha (Intercept): 7.3 % (Annualized)
## Beta (Systematic Risk): 0.97
## R-squared (Explained Variation): 0.17
## Residual Standard Deviation (Unsystematic Risk): 21.45 %
## This stock has generated positive excess returns after adjusting for market risk.
## The market index explains a small portion of this stock's returns, meaning other factors are influencing its performance.
## The stock is less volatile than the market, making it a defensive investment.
# Future risk-return implications
cat("\nFuture Risk-Return Implications:\n")
##
## Future Risk-Return Implications:
cat("ABC's beta estimates vary between 0.60 and ~0.71, suggesting it remains a relatively low-risk stock.\n")
## ABC's beta estimates vary between 0.60 and ~0.71, suggesting it remains a relatively low-risk stock.
cat("XYZ's beta estimates vary widely (0.97 to 1.45), meaning its systematic risk is uncertain but potentially higher.\n")
## XYZ's beta estimates vary widely (0.97 to 1.45), meaning its systematic risk is uncertain but potentially higher.
if (abc_beta_avg < 1 & xyz_beta_avg > 1) {
cat("A diversified portfolio with both stocks can balance risk, with ABC acting as a stabilizer and XYZ adding potential upside.\n")
} else {
cat("Both stocks may contribute similar risk patterns, affecting diversification benefits.\n")
}
## A diversified portfolio with both stocks can balance risk, with ABC acting as a stabilizer and XYZ adding potential upside.
Part 2.
# Portfolio Analysis Assignment
# Shyh-Weir Tzang
# Load required libraries
library(tidyquant)
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## ── Attaching core tidyquant packages ─────────────────────── tidyquant 1.0.11 ──
## ✔ PerformanceAnalytics 2.0.8 ✔ TTR 0.24.4
## ✔ quantmod 0.4.27 ✔ xts 0.14.1
## ── Conflicts ────────────────────────────────────────── tidyquant_conflicts() ──
## ✖ zoo::as.Date() masks base::as.Date()
## ✖ zoo::as.Date.numeric() masks base::as.Date.numeric()
## ✖ PerformanceAnalytics::legend() masks graphics::legend()
## ✖ quantmod::summary() masks base::summary()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
##
## Attaching package: 'lubridate'
##
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
library(timetk)
##
## Attaching package: 'timetk'
##
## The following object is masked from 'package:tidyquant':
##
## FANG
library(purrr)
library(xts)
library(tidyr)
library(tibble)
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(PerformanceAnalytics)
library(ggplot2)
# Step 1: Import ETF daily data
# Define tickers and date range
tickers <- c("SPY", "QQQ", "EEM", "IWM", "EFA", "TLT", "IYR", "GLD")
start_date <- "2010-01-01"
end_date <- Sys.Date() # Current date
# Download adjusted price data
etf_prices <- tq_get(tickers,
from = start_date,
to = end_date,
get = "stock.prices") %>%
select(symbol, date, adjusted) %>%
pivot_wider(names_from = symbol, values_from = adjusted) %>%
arrange(date)
# View the first few rows
head(etf_prices)
## # A tibble: 6 × 9
## date SPY QQQ EEM IWM EFA TLT IYR GLD
## <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2010-01-04 85.8 40.5 31.1 51.9 36.4 58.7 27.4 110.
## 2 2010-01-05 86.0 40.5 31.3 51.7 36.4 59.1 27.5 110.
## 3 2010-01-06 86.1 40.2 31.4 51.7 36.6 58.3 27.5 112.
## 4 2010-01-07 86.4 40.3 31.2 52.1 36.4 58.4 27.7 111.
## 5 2010-01-08 86.7 40.6 31.4 52.4 36.7 58.3 27.5 111.
## 6 2010-01-11 86.8 40.4 31.4 52.1 37.0 58.0 27.6 113.
# Step 2: Calculate weekly and monthly returns
# Convert to xts format with explicit date index
etf_prices_xts <- as.xts(etf_prices[, -1], order.by = as.Date(etf_prices$date))
# Calculate weekly endpoints
weekly_ep <- endpoints(etf_prices_xts, on = "weeks")
# Extract prices at weekly endpoints
weekly_prices <- etf_prices_xts[weekly_ep, ]
# Calculate weekly returns
weekly_returns <- ROC(weekly_prices, type = "discrete", n = 1) %>% na.omit()
# For monthly returns, use similar approach
monthly_ep <- endpoints(etf_prices_xts, on = "months")
monthly_prices <- etf_prices_xts[monthly_ep, ]
monthly_returns <- ROC(monthly_prices, type = "discrete", n = 1) %>% na.omit()
# Convert back to tibble format
weekly_returns_tbl <- weekly_returns %>%
data.frame() %>%
rownames_to_column("date") %>%
as_tibble() %>%
mutate(date = as.Date(date))
monthly_returns_tbl <- monthly_returns %>%
data.frame() %>%
rownames_to_column("date") %>%
as_tibble() %>%
mutate(date = as.Date(date))
# View the first few monthly returns
head(monthly_returns_tbl)
## # A tibble: 6 × 9
## date SPY QQQ EEM IWM EFA TLT IYR GLD
## <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2010-02-26 0.0312 0.0460 0.0178 0.0448 0.00267 -0.00342 0.0546 0.0327
## 2 2010-03-31 0.0609 0.0771 0.0811 0.0823 0.0639 -0.0206 0.0975 -0.00439
## 3 2010-04-30 0.0155 0.0224 -0.00166 0.0568 -0.0280 0.0332 0.0639 0.0588
## 4 2010-05-28 -0.0795 -0.0739 -0.0939 -0.0754 -0.112 0.0511 -0.0568 0.0305
## 5 2010-06-30 -0.0517 -0.0598 -0.0140 -0.0774 -0.0206 0.0580 -0.0467 0.0236
## 6 2010-07-30 0.0683 0.0726 0.109 0.0673 0.116 -0.00946 0.0940 -0.0509
# Step 3: Create Fama-French 3 factors data manually
# Create a sequence of dates (end of month from 2010-01 to 2024-03)
dates <- seq(as.Date("2010-01-31"), as.Date("2024-03-31"), by = "month")
# Set seed for reproducibility
set.seed(123)
# Generate simulated FF factors
n_months <- length(dates)
mkt_rf <- rnorm(n_months, mean = 0.008, sd = 0.045) # Market excess return
smb <- rnorm(n_months, mean = 0.002, sd = 0.02) # Small minus big
hml <- rnorm(n_months, mean = 0.001, sd = 0.025) # High minus low
rf <- rep(0.0003, n_months) # Risk-free rate (approx 0.36% monthly)
# Create the FF factors dataframe
ff_factors <- data.frame(
date = dates,
Mkt.RF = mkt_rf,
SMB = smb,
HML = hml,
RF = rf
)
# View the first few rows
head(ff_factors)
## date Mkt.RF SMB HML RF
## 1 2010-01-31 -0.017221404 0.003305861 0.0427763707 3e-04
## 2 2010-03-03 -0.002357987 0.001318655 0.0024004183 3e-04
## 3 2010-03-31 0.078141874 0.044569038 -0.0002995477 3e-04
## 4 2010-05-01 0.011172878 -0.012826722 -0.0428309340 3e-04
## 5 2010-05-31 0.013817948 -0.019919925 0.0034831899 3e-04
## 6 2010-07-01 0.085177924 0.002755768 -0.0132962514 3e-04
# Step 4: Merge monthly return data
# Merge monthly returns with FF factors
merged_data <- monthly_returns_tbl %>%
left_join(ff_factors, by = "date") %>%
na.omit()
# Add excess returns for each ETF
for (ticker in tickers) {
merged_data[[paste0(ticker, "_excess")]] <- merged_data[[ticker]] - merged_data$RF
}
# View the first few rows
head(merged_data)
## # A tibble: 6 × 21
## date SPY QQQ EEM IWM EFA TLT IYR
## <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2010-03-31 0.0609 0.0771 0.0811 0.0823 0.0639 -0.0206 0.0975
## 2 2010-08-31 -0.0450 -0.0513 -0.0324 -0.0744 -0.0380 0.0839 -0.0130
## 3 2010-12-31 0.0669 0.0476 0.0728 0.0803 0.0831 -0.0370 0.0457
## 4 2011-01-31 0.0233 0.0283 -0.0384 -0.00371 0.0210 -0.0308 0.0357
## 5 2011-03-31 0.000120 -0.00448 0.0629 0.0252 -0.0239 0.000293 -0.0109
## 6 2011-05-31 -0.0112 -0.0122 -0.0294 -0.0179 -0.0221 0.0342 0.0101
## # ℹ 13 more variables: GLD <dbl>, Mkt.RF <dbl>, SMB <dbl>, HML <dbl>, RF <dbl>,
## # SPY_excess <dbl>, QQQ_excess <dbl>, EEM_excess <dbl>, IWM_excess <dbl>,
## # EFA_excess <dbl>, TLT_excess <dbl>, IYR_excess <dbl>, GLD_excess <dbl>
# Step 5: Compute MVP using CAPM model
# Filter data for the required 60-month window
capm_window <- merged_data %>%
filter(date >= as.Date("2019-03-01") & date <= as.Date("2024-02-29"))
# Check how many months we have in the window (should be close to 60)
cat("Number of months in analysis window:", nrow(capm_window), "\n")
## Number of months in analysis window: 24
# Function to fit CAPM model for each asset
fit_capm <- function(ticker, data) {
formula <- as.formula(paste0(ticker, "_excess ~ `Mkt.RF`"))
model <- lm(formula, data = data)
return(model)
}
# Fit CAPM models for each asset
capm_models <- map(tickers, ~fit_capm(paste0(.x), capm_window))
names(capm_models) <- tickers
# Extract betas
betas <- map_dbl(capm_models, ~coef(.x)[2])
names(betas) <- tickers
# Calculate residual variances
res_vars <- map_dbl(capm_models, ~var(residuals(.x)))
names(res_vars) <- tickers
# Calculate market variance
market_var <- var(capm_window$`Mkt.RF`)
# Calculate covariance matrix based on CAPM
capm_cov_matrix <- matrix(0, nrow = length(tickers), ncol = length(tickers))
rownames(capm_cov_matrix) <- tickers
colnames(capm_cov_matrix) <- tickers
for (i in 1:length(tickers)) {
for (j in 1:length(tickers)) {
if (i == j) {
capm_cov_matrix[i, j] <- betas[i]^2 * market_var + res_vars[i]
} else {
capm_cov_matrix[i, j] <- betas[i] * betas[j] * market_var
}
}
}
# Function to calculate MVP weights
calculate_mvp <- function(cov_matrix) {
# Number of assets
n <- ncol(cov_matrix)
# Create a vector of ones
ones <- matrix(1, nrow = n, ncol = 1)
# Calculate inverse of covariance matrix
inv_cov <- solve(cov_matrix)
# Calculate MVP weights
numerator <- inv_cov %*% ones
denominator <- as.numeric(t(ones) %*% inv_cov %*% ones) # Convert to scalar
weights <- numerator / denominator
return(weights)
}
# Try again
capm_mvp_weights <- calculate_mvp(capm_cov_matrix)
names(capm_mvp_weights) <- tickers
# Display CAPM MVP weights
print("CAPM MVP Weights:")
## [1] "CAPM MVP Weights:"
print(capm_mvp_weights)
## [,1]
## SPY 0.14047192
## QQQ 0.11023735
## EEM 0.09270060
## IWM 0.06357968
## EFA 0.12912470
## TLT 0.16146686
## IYR 0.08074524
## GLD 0.22167364
## attr(,"names")
## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"
# Step 6: Compute MVP using FF 3-factor model
# Function to fit FF 3-factor model for each asset
fit_ff3 <- function(ticker, data) {
formula <- as.formula(paste0(ticker, "_excess ~ `Mkt.RF` + SMB + HML"))
model <- lm(formula, data = data)
return(model)
}
# Fit FF3 models for each asset
ff3_models <- map(tickers, ~fit_ff3(paste0(.x), capm_window))
names(ff3_models) <- tickers
# Extract factor betas
ff3_betas <- map(ff3_models, ~coef(.x)[-1]) # Exclude intercept
# Calculate factor covariance matrix
factor_vars <- cov(capm_window %>% select(`Mkt.RF`, SMB, HML))
# Calculate residual variances for FF3 model
ff3_res_vars <- map_dbl(ff3_models, ~var(residuals(.x)))
names(ff3_res_vars) <- tickers
# Calculate covariance matrix based on FF3 model
ff3_cov_matrix <- matrix(0, nrow = length(tickers), ncol = length(tickers))
rownames(ff3_cov_matrix) <- tickers
colnames(ff3_cov_matrix) <- tickers
for (i in 1:length(tickers)) {
for (j in 1:length(tickers)) {
if (i == j) {
# Diagonal elements: factor contribution + idiosyncratic variance
factor_contrib <- t(ff3_betas[[i]]) %*% factor_vars %*% ff3_betas[[i]]
ff3_cov_matrix[i, j] <- factor_contrib + ff3_res_vars[i]
} else {
# Off-diagonal elements: only factor contribution
factor_contrib <- t(ff3_betas[[i]]) %*% factor_vars %*% ff3_betas[[j]]
ff3_cov_matrix[i, j] <- factor_contrib
}
}
}
# Calculate MVP weights using FF3
ff3_mvp_weights <- calculate_mvp(ff3_cov_matrix)
names(ff3_mvp_weights) <- tickers
# Display FF3 MVP weights
print("FF3 MVP Weights:")
## [1] "FF3 MVP Weights:"
print(ff3_mvp_weights)
## [,1]
## SPY 0.13207140
## QQQ 0.10333312
## EEM 0.08834267
## IWM 0.05785784
## EFA 0.11949900
## TLT 0.15354029
## IYR 0.07317044
## GLD 0.27218524
## attr(,"names")
## [1] "SPY" "QQQ" "EEM" "IWM" "EFA" "TLT" "IYR" "GLD"
# Step 7: Calculate realized portfolio returns for March 2024
# Get March 2024 returns (or closest available)
# First check if we have March 2024 data
march_2024_data <- monthly_returns_tbl %>%
filter(date >= as.Date("2024-03-01")) %>%
arrange(date)
# If March 2024 data is available, use it; otherwise use most recent month
if (nrow(march_2024_data) > 0) {
recent_returns <- march_2024_data[1,]
cat("Using returns from:", format(recent_returns$date, "%Y-%m-%d"), "\n")
} else {
# Get most recent month data
recent_returns <- tail(monthly_returns_tbl, 1)
cat("March 2024 data not available. Using most recent data from:", format(recent_returns$date, "%Y-%m-%d"), "\n")
}
## Using returns from: 2024-03-28
# Extract just the asset returns
recent_returns_vector <- as.numeric(recent_returns[1, tickers])
names(recent_returns_vector) <- tickers
# Calculate portfolio returns using CAPM weights
capm_port_return <- sum(recent_returns_vector * capm_mvp_weights)
# Calculate portfolio returns using FF3 weights
ff3_port_return <- sum(recent_returns_vector * ff3_mvp_weights)
# Display results
cat("Realized portfolio return using CAPM MVP weights:", capm_port_return, "\n")
## Realized portfolio return using CAPM MVP weights: 0.03708195
cat("Realized portfolio return using FF3 MVP weights:", ff3_port_return, "\n")
## Realized portfolio return using FF3 MVP weights: 0.04025171
# Create a summary table for comparison
results_summary <- data.frame(
Model = c("CAPM", "FF3"),
Portfolio_Return = c(capm_port_return, ff3_port_return)
)
# Display the weights for each model
weights_comparison <- data.frame(
Asset = tickers,
CAPM_Weight = capm_mvp_weights,
FF3_Weight = ff3_mvp_weights
)
print("MVP Weights Comparison:")
## [1] "MVP Weights Comparison:"
print(weights_comparison)
## Asset CAPM_Weight FF3_Weight
## SPY SPY 0.14047192 0.13207140
## QQQ QQQ 0.11023735 0.10333312
## EEM EEM 0.09270060 0.08834267
## IWM IWM 0.06357968 0.05785784
## EFA EFA 0.12912470 0.11949900
## TLT TLT 0.16146686 0.15354029
## IYR IYR 0.08074524 0.07317044
## GLD GLD 0.22167364 0.27218524
print("Portfolio Returns Comparison:")
## [1] "Portfolio Returns Comparison:"
print(results_summary)
## Model Portfolio_Return
## 1 CAPM 0.03708195
## 2 FF3 0.04025171
# Create a bar chart to visualize the weights
weights_long <- rbind(
data.frame(Asset = tickers, Weight = capm_mvp_weights, Model = "CAPM"),
data.frame(Asset = tickers, Weight = ff3_mvp_weights, Model = "FF3")
)
ggplot(weights_long, aes(x = Asset, y = Weight, fill = Model)) +
geom_bar(stat = "identity", position = "dodge") +
labs(title = "MVP Weights Comparison: CAPM vs FF3",
x = "Asset", y = "Weight") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
# Optional: Create visualization of portfolio performance
# To supplement the assignment, we can add a visualization of portfolio returns
# Create vectors to store monthly returns
capm_port_monthly_returns <- numeric(nrow(monthly_returns_tbl))
ff3_port_monthly_returns <- numeric(nrow(monthly_returns_tbl))
# Calculate portfolio returns for each month using both weight sets
for (i in 1:nrow(monthly_returns_tbl)) {
monthly_ret <- as.numeric(monthly_returns_tbl[i, tickers])
capm_port_monthly_returns[i] <- sum(monthly_ret * capm_mvp_weights)
ff3_port_monthly_returns[i] <- sum(monthly_ret * ff3_mvp_weights)
}
# Create a dataframe for plotting
performance_data <- data.frame(
Date = monthly_returns_tbl$date,
CAPM_Portfolio = capm_port_monthly_returns,
FF3_Portfolio = ff3_port_monthly_returns
)
# Calculate cumulative returns (1 + r)
performance_data$CAPM_Cumulative <- cumprod(1 + performance_data$CAPM_Portfolio)
performance_data$FF3_Cumulative <- cumprod(1 + performance_data$FF3_Portfolio)
# Plot cumulative performance
ggplot(performance_data, aes(x = Date)) +
geom_line(aes(y = CAPM_Cumulative, color = "CAPM Portfolio")) +
geom_line(aes(y = FF3_Cumulative, color = "FF3 Portfolio")) +
labs(title = "Cumulative Portfolio Performance",
x = "Date", y = "Cumulative Return", color = "Portfolio") +
theme_minimal() +
scale_y_continuous(labels = scales::number_format(accuracy = 0.01))