This project includes risk metrics, portfolio optimization, and backtesting on an hypothetical investment portfolio.
2025-05-01
library(quantmod)
library(PerformanceAnalytics)
library(PortfolioAnalytics)
library(ROI)
library(ROI.plugin.quadprog)
library(ROI.plugin.glpk)
library(ROI.plugin.symphony)
library(CVXR)
library(corrplot)
# Define ticker symbols
tickers <- c("AAPL", "MSFT", "GOOGL", "JPM", "XOM")
# Get 5 years of monthly price data
getSymbols(tickers,
src = "yahoo",
from = Sys.Date() - 365*5)
[1] "AAPL" "MSFT" "GOOGL" "JPM" "XOM"
prices <- do.call(merge, lapply(tickers, function(sym) Ad(get(sym))))
colnames(prices) <- tickers
returns <- na.omit(Return.calculate(prices))
# Performance summary
charts.PerformanceSummary(returns,
main = "Daily Returns",
legend.loc = "topleft")
# Risk metrics
table.Stats(returns)
AAPL MSFT GOOGL JPM XOM
Observations 1254 1254 1254 1254 1254
NAs 0.0000 0.0000 0.0000 0.0000 0.0000
Minimum -0.0925 -0.0772 -0.0951 -0.0834 -0.0883
Quartile 1 -0.0082 -0.0080 -0.0096 -0.0079 -0.0105
Median 0.0013 0.0009 0.0015 0.0010 0.0007
Arithmetic Mean 0.0010 0.0008 0.0009 0.0010 0.0011
Geometric Mean 0.0009 0.0007 0.0007 0.0009 0.0009
Quartile 3 0.0118 0.0106 0.0115 0.0099 0.0118
Maximum 0.1533 0.1013 0.1022 0.1354 0.1266
SE Mean 0.0005 0.0005 0.0005 0.0005 0.0005
LCL Mean (0.95) 0.0000 -0.0001 -0.0002 0.0001 0.0000
UCL Mean (0.95) 0.0021 0.0018 0.0020 0.0020 0.0021
Variance 0.0004 0.0003 0.0004 0.0003 0.0004
Stdev 0.0188 0.0170 0.0194 0.0171 0.0193
Skewness 0.3530 0.0482 0.0080 0.5364 0.2047
Kurtosis 5.6753 2.5586 2.9314 6.5594 2.6217
table.AnnualizedReturns(returns)
AAPL MSFT GOOGL JPM XOM
Annualized Return 0.2459 0.1831 0.1936 0.2513 0.2457
Annualized Std Dev 0.2986 0.2696 0.3087 0.2721 0.3066
Annualized Sharpe (Rf=0%) 0.8236 0.6793 0.6270 0.9235 0.8012
table.DownsideRisk(returns)
AAPL MSFT GOOGL JPM XOM
Semi Deviation 0.0132 0.0121 0.0139 0.0118 0.0134
Gain Deviation 0.0134 0.0114 0.0131 0.0127 0.0134
Loss Deviation 0.0129 0.0116 0.0134 0.0114 0.0123
Downside Deviation (MAR=210%) 0.0174 0.0165 0.0181 0.0161 0.0177
Downside Deviation (Rf=0%) 0.0127 0.0117 0.0134 0.0113 0.0129
Downside Deviation (0%) 0.0127 0.0117 0.0134 0.0113 0.0129
Maximum Drawdown 0.3336 0.3715 0.4432 0.3877 0.4119
Historical VaR (95%) -0.0298 -0.0270 -0.0307 -0.0263 -0.0291
Historical ES (95%) -0.0416 -0.0379 -0.0436 -0.0372 -0.0410
Modified VaR (95%) -0.0258 -0.0260 -0.0299 -0.0222 -0.0285
Modified ES (95%) -0.0313 -0.0378 -0.0446 -0.0222 -0.0394
VaR(returns, p = 0.95, method = "historical")
AAPL MSFT GOOGL JPM XOM
VaR -0.02979278 -0.02697372 -0.03074297 -0.02632342 -0.02911001
ES(returns, p = 0.95, method = "historical")
AAPL MSFT GOOGL JPM XOM
ES -0.04163966 -0.03788462 -0.04364204 -0.03722302 -0.04097581
# Correlation matrix
cor_matrix <- cor(returns)
corrplot::corrplot(cor_matrix,
method = "circle",
type = "upper")
# Covariance matrix
cov_matrix <- cov(returns)
Microsoft, Google and Apple are the assets with the highest positive correlation while JPMorgan Chase & Co and Exxon Mobil Corp have little to no correlation with other stocks.
# Define Portfolio
port_spec <- portfolio.spec(assets = colnames(returns))
# Add constraints
port_spec <- add.constraint(portfolio = port_spec,
type = "full_investment")
port_spec <- add.constraint(portfolio = port_spec,
type = "long_only")
# Add objectives
port_spec <- add.objective(portfolio = port_spec,
type = "risk",
name = "StdDev")
port_spec <- add.objective(portfolio = port_spec,
type = "return",
name = "mean")
# Optimization
opt_result <- optimize.portfolio(R = returns,
portfolio = port_spec,
optimize_method = "ROI",
trace = TRUE)
opt_result
***********************************
PortfolioAnalytics Optimization
***********************************
Call:
optimize.portfolio(R = returns, portfolio = port_spec, optimize_method = "ROI",
trace = TRUE)
Optimal Weights:
AAPL MSFT GOOGL JPM XOM
0.3786 0.0000 0.0000 0.3163 0.3051
Objective Measure:
mean
0.001048
StdDev
0.01358
chart.Weights(opt_result)
# Equal-weight portfolio returns
equal_weights <- rep(1 / ncol(returns), ncol(returns))
ew_returns <- Return.portfolio(returns,
weights = equal_weights)
# Optimized portfolio returns
opt_weights <- extractWeights(opt_result)
opt_returns <- Return.portfolio(returns,
weights = opt_weights)
# Combine and plot
combined <- merge.xts(ew_returns, opt_returns)
colnames(combined) <- c("Equal Weight", "Optimized")
charts.PerformanceSummary(combined, main = "Performance: Equal Weight vs Optimized")
# Efficient frontier
ef <- create.EfficientFrontier(returns,
port_spec,
type = "mean-StdDev")
chart.EfficientFrontier(ef,
match.col = "StdDev",
n.portfolios = 25,
main = "Efficient Frontier",
xlim = c(0.012, 0.022),
ylim = c(0.0008, 0.0011))
Given the correlations observed earlier, which were all positive, it is no surprise that only one stock among Apple, Microsoft, and Google was selected for the optimized portfolio.
Reviewing the annualized returns and volatility calculated at the beginning:
AAPL: - annualized return: 0.2459 - volatility: 0.2986 - Sharpe ratio: 0.8236
MSFT: - annualized return:0.1831 - volatility: 0.2696 - Sharpe ratio: 0.6793
GOOGL: - annualized return:0.1936 - volatility: 0.3087 - Sharpe ratio: 0.6270
AAPL’s higher annualized return and Sharpe ratio, compared to its counterparts, can explain why it was chosen among the three highly correlated stocks.
opt_weights <- as.numeric(opt_weights)
# Portfolio volatility
port_volatility <- sqrt(sum(opt_weights * (cov_matrix %*% opt_weights)))
# Risk contribution for each asset
risk_contrib <- (cov_matrix %*% opt_weights) * opt_weights / port_volatility
# Convert to a data frame
risk_contrib_df <- data.frame(Asset = colnames(returns), RiskContribution = risk_contrib)
print(risk_contrib_df)
Asset RiskContribution
AAPL 0.005161189
MSFT 0.000000000
GOOGL 0.000000000
JPM 0.004157583
XOM 0.004258789
The risk contribution table can be directly explained by weight allocation seen above.
benchmark_returns <- Return.portfolio(R = returns,
weights = equal_weights,
rebalance_on = "years")
colnames(benchmark_returns) <-"benchmark"
base_port_spec <- port_spec
opt_base <- optimize.portfolio.rebalancing(R = returns,
optimize_method = "ROI",
portfolio = base_port_spec,
rebalance_on = "quarters",
training_period = 60,
rolling_window = 60)
base_returns <- Return.portfolio(returns, extractWeights(opt_base))
colnames(base_returns) <- "base"
chart.Weights(opt_base, main = "Unconstrained Weights")
box_port_spec <- port_spec
box_port_spec <- add.constraint(portfolio = box_port_spec,
type = "box",
min = 0.05, max = 0.4,
indexnum = 2)
opt_box <- optimize.portfolio.rebalancing(R = returns,
optimize_method = "ROI",
portfolio = box_port_spec,
rebalance_on = "quarters",
training_period = 60,
rolling_window = 60)
box_returns <- Return.portfolio(returns, extractWeights(opt_box))
colnames(box_returns) <- "box"
chart.Weights(opt_box, main = "Box Weights")
ret <- cbind(benchmark_returns, base_returns, box_returns)
table.AnnualizedReturns(ret)
benchmark base box
Annualized Return 0.2587 0.2034 0.1735
Annualized Std Dev 0.2038 0.2662 0.2132
Annualized Sharpe (Rf=0%) 1.2695 0.7640 0.8138
Base Weights: unconstrained optimizer
Box Weights: box constraints (min 5%, max 40% per asset)