Portfolio Optimization in R

This project includes risk metrics, portfolio optimization, and backtesting on an hypothetical investment portfolio.


Load Required Packages

library(quantmod)
library(PerformanceAnalytics)
library(PortfolioAnalytics)   
library(ROI)             
library(ROI.plugin.quadprog)  
library(ROI.plugin.glpk)
library(ROI.plugin.symphony)
library(CVXR)
library(corrplot)  
  • quantmod for financial data retrieval
  • PerformanceAnalytics for performance measurement
  • PortfolioAnalytics for portfolio optimization framework
  • ROI.plugin.quadprog for quadratic programming solver

Data Acquisition and Preprocessing

# 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))
  • Data: 5 years of monthly adjusted closing prices from Yahoo Finance.
  • Returns: Calculated as log returns, cleaned for NAs.

Performance Analysis and Risk Metrics

# 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 Analysis

# 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.


Portfolio Optimization Setup

# 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 Results and Efficient Frontier

# 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.


Risk Contribution Analysis

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.


Backtesting

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)

  • The unconstrained weighted portfolio shows extreme concentration patterns, often allocating nearly 100% to a single asset. This explains both the higher volatility and lower Sharpe ratio compared to the benchmark.
  • The box weighted portfolio shows a more balanced distribution among all five assets throughout the period. This results in lower volatility than the base portfolio, improving the Sharpe ratio despite lower returns.

Key Takeaways

  • Unconstrained optimization can lead to extreme, concentrated bets, increasing risk and sometimes reducing risk-adjusted returns.
  • Box constraints force diversification, lowering risk and often improving the Sharpe ratio, even if raw returns decrease.
  • Simple strategies like equal-weighting can outperform more complex optimization in both return and risk-adjusted terms, especially when optimizer overfit or ignore diversification.