1 Introduction

In this project, I strived to produce an introductory guide to the PerformanceAnalytics package, which is commonly used in R for portfolio analysis. I wanted to make this guide partly because I had been confused by what the functions in the package does some times, and so it also serves to explore what the function is about.

I only covered the main functions that I used frequently, and therefore the guide is only somewhat complete as the title suggests. I may update this guide as I learn and use the other functions more frequently.

2 Packages Required

library(dplyr) # For data manipulation and piping function
library(PerformanceAnalytics) # For portfolio analysis and return calculation
library(quantmod) # For retrieving data 

3 quantmod Package

Before delving into the PerformanceAnalytics and PortfolioAnalytics packages, I would briefly discuss how to retrieve stock data from Yahoo Finance using the quantmod package. The data retrieved here would be used for the rest of this project.

The basic introduction to the quantmod package in this section should be enough for most people to start exploring equity portfolio analysis in R.

3.1 Retrieve Single Stock

Retrieve historical data of SPDR S&P500 ETF (SPY):

spy_daily <- quantmod::getSymbols(Symbols = "SPY", src = "yahoo", auto.assign = FALSE)

head(spy_daily)
##            SPY.Open SPY.High SPY.Low SPY.Close SPY.Volume SPY.Adjusted
## 2007-01-03   142.25   142.86  140.57    141.37   94807600     104.3016
## 2007-01-04   141.23   142.05  140.61    141.67   69620600     104.5229
## 2007-01-05   141.33   141.40  140.38    140.54   76645300     103.6892
## 2007-01-08   140.82   141.41  140.25    141.19   71655000     104.1688
## 2007-01-09   141.31   141.60  140.40    141.07   75680100     104.0802
## 2007-01-10   140.58   141.57  140.30    141.54   72428000     104.4270
# Check start and end date of data
start(spy_daily); end(spy_daily)
## [1] "2007-01-03"
## [1] "2022-08-02"

The quantmod::getSymbols() retrieves the daily historical data of SPY, consisting of its daily open, high, low, close, volume and adjusted close. By default, the function retrieves data from 03 January 2007 to the date when the function was ran, based on Sys.Date() (common to lag by a day due to timezone differences).

We can retrieve data starting and ending on a specific date and change the periodicity of the data by including the arguments from, to and periodicity.

Let’s retrieve the monthly data of iShares Russell 2000 ETF (IWM) from 01 January 2005 to 01 July 2022:

iwm_monthly <- quantmod::getSymbols(Symbols = "IWM", src = "yahoo", auto.assign = FALSE, 
                                    from = as.Date("2005-01-01"), to = as.Date("2022-07-01"), periodicity = "monthly")

head(iwm_monthly)
##            IWM.Open IWM.High IWM.Low IWM.Close IWM.Volume IWM.Adjusted
## 2005-01-01   65.095   65.210  60.085    62.120  432010200     49.20217
## 2005-02-01   62.125   63.900  61.385    63.140  340127800     50.01005
## 2005-03-01   63.330   64.640  59.925    61.075  419398000     48.37448
## 2005-04-01   61.365   61.805  56.495    57.620  623098600     45.84352
## 2005-05-01   57.780   61.705  57.445    61.345  549871000     48.80721
## 2005-06-01   61.340   64.650  61.320    63.700  439086100     50.68088
# Check start and end date of data
start(iwm_monthly); end(iwm_monthly)
## [1] "2005-01-01"
## [1] "2022-06-01"

We can see that the monthly data retrieved ends in June 2022 instead of July 2022. Furthermore, it returned with the dates of the first day of each month instead of the last trading day of each month, which can be confusing to some. Another way would be to use xts::to.period() and specify the periodicity to convert to a lower periodicity from daily data.

Convert SPY daily data to weekly, monthly, quarterly and yearly periodicity using xts::to.period():

# Convert to weekly series
spy_weekly <- xts::to.period(x = spy_daily, period = "weeks")

head(spy_weekly)
##            spy_daily.Open spy_daily.High spy_daily.Low spy_daily.Close
## 2007-01-05         142.25         142.86        140.38          140.54
## 2007-01-12         140.82         143.24        140.25          143.24
## 2007-01-19         143.07         143.46        142.31          142.82
## 2007-01-26         143.07         143.98        141.58          142.13
## 2007-02-02         142.19         144.95        141.74          144.81
## 2007-02-09         144.70         145.36        143.39          143.94
##            spy_daily.Volume spy_daily.Adjusted
## 2007-01-05        241073500           103.6892
## 2007-01-12        329610500           105.6813
## 2007-01-19        220263000           105.3714
## 2007-01-26        310992100           104.8623
## 2007-02-02        347310200           106.8396
## 2007-02-09        308181700           106.1977
# Convert to monthly series
spy_monthly <- xts::to.period(x = spy_daily, period = "months")

head(spy_monthly)
##            spy_daily.Open spy_daily.High spy_daily.Low spy_daily.Close
## 2007-01-31         142.25         144.13        140.25          143.75
## 2007-02-28         144.15         146.42        139.00          140.93
## 2007-03-30         139.34         143.81        136.75          142.00
## 2007-04-30         142.16         149.80        141.48          148.29
## 2007-05-31         148.42         153.89        147.67          153.32
## 2007-06-29         153.88         154.40        148.06          150.43
##            spy_daily.Volume spy_daily.Adjusted
## 2007-01-31       1330329900           106.0575
## 2007-02-28       1494548900           103.9770
## 2007-03-30       2918304400           105.1820
## 2007-04-30       1791289900           109.8410
## 2007-05-31       2508178000           113.5669
## 2007-06-29       3502885400           111.9064
# Convert to weekly series
spy_quarterly <- xts::to.period(x = spy_daily, period = "quarters")

head(spy_quarterly)
##            spy_daily.Open spy_daily.High spy_daily.Low spy_daily.Close
## 2007-03-30         142.25         146.42        136.75          142.00
## 2007-06-29         142.16         154.40        141.48          150.43
## 2007-09-28         150.87         155.53        137.00          152.58
## 2007-12-31         152.60         157.52        140.66          146.21
## 2008-03-31         146.53         146.99        126.00          131.97
## 2008-06-30         133.61         144.30        127.04          127.98
##            spy_daily.Volume spy_daily.Adjusted
## 2007-03-30       5743183200          105.18195
## 2007-06-29       7802353300          111.90643
## 2007-09-28      13151604000          114.04436
## 2007-12-31      12616567000          109.86313
## 2008-03-31      15719984700           99.65404
## 2008-06-30      13070579600           97.12445
# Convert to weekly series
spy_yearly <- xts::to.period(x = spy_daily, period = "years")

head(spy_yearly)
##            spy_daily.Open spy_daily.High spy_daily.Low spy_daily.Close
## 2007-12-31         142.25         157.52        136.75          146.21
## 2008-12-31         146.53         146.99         74.34           90.24
## 2009-12-31          90.44         113.03         67.10          111.44
## 2010-12-31         112.37         126.20        101.13          125.75
## 2011-12-30         126.71         137.18        107.43          125.50
## 2012-12-31         127.76         148.11        126.43          142.41
##            spy_daily.Volume spy_daily.Adjusted
## 2007-12-31      39313707500          109.86313
## 2008-12-31      75960832400           69.43897
## 2009-12-31      62062517700           87.73737
## 2010-12-31      52842437000          100.94727
## 2011-12-30      54991433900          102.86021
## 2012-12-31      35832734200          119.30789

Using the xts::to.period() uses the last date of each period, unlike the case with the IWM data, which might be more intuitive to some people.

3.2 Data Manipulation

The data we have are xts objects, making subsetting of dates easier. This cheatsheet by DataCamp summarizes the different functions to use with xts objects.

The IWM data is used as an example in subsetting.

# Subset 2013 data
iwm_monthly["2013"]
##            IWM.Open IWM.High IWM.Low IWM.Close IWM.Volume IWM.Adjusted
## 2013-01-01    86.34    90.23   86.04     89.58  766259000     79.19477
## 2013-02-01    89.96    92.68   88.79     90.48  583143300     79.99043
## 2013-03-01    89.88    94.96   89.21     94.43  646279900     83.48247
## 2013-04-01    94.27    94.57   89.13     94.10 1052706400     83.42464
## 2013-05-01    93.81   100.38   91.76     97.80  896201900     86.70488
## 2013-06-01    98.14    99.80   93.83     97.00  996340700     85.99561
## 2013-07-01    97.65   104.98   97.45    103.66  690073000     91.90009
## 2013-08-01   104.68   105.63  100.24    100.38  641771700     89.38205
## 2013-09-01   101.86   107.61  100.13    106.61  685107200     94.92950
## 2013-10-01   106.56   111.62  103.00    109.19  882360000     97.48563
## 2013-11-01   109.41   114.16  107.14    113.51  749041900    101.34256
## 2013-12-01   113.70   115.97  109.32    115.36  741583600    102.99425
# Subset 2013 October to 2014 June data
iwm_monthly["2013-10/2014-06"]
##            IWM.Open IWM.High IWM.Low IWM.Close IWM.Volume IWM.Adjusted
## 2013-10-01   106.56   111.62  103.00    109.19  882360000     97.48563
## 2013-11-01   109.41   114.16  107.14    113.51  749041900    101.34256
## 2013-12-01   113.70   115.97  109.32    115.36  741583600    102.99425
## 2014-01-01   115.09   117.37  111.02    112.16  792854000    100.52286
## 2014-02-01   111.87   118.71  107.27    117.52  844601900    105.32671
## 2014-03-01   116.52   120.58  113.69    116.34 1045242100    104.26914
## 2014-04-01   116.63   118.48  108.66    111.98 1116610700    100.62065
## 2014-05-01   111.75   113.84  107.44    112.86 1099414400    101.41139
## 2014-06-01   113.07   118.91  111.11    118.81  790245900    106.75779

With daily data, we can also include the day within the subset command. There may be variations for subsetting depending on the index in the xts object.

To subset columns, instead of using iwm_monthly[, 1] for the IWM.Open or iwm_monthly[, 5] for the IWM.Volume columns, we can use functions from the quantmod package.

# Subset opening prices
quantmod::Op(iwm_monthly) %>% head()
##            IWM.Open
## 2005-01-01   65.095
## 2005-02-01   62.125
## 2005-03-01   63.330
## 2005-04-01   61.365
## 2005-05-01   57.780
## 2005-06-01   61.340
# Subset volume
quantmod::Vo(iwm_monthly) %>% head()
##            IWM.Volume
## 2005-01-01  432010200
## 2005-02-01  340127800
## 2005-03-01  419398000
## 2005-04-01  623098600
## 2005-05-01  549871000
## 2005-06-01  439086100
# Other similar functions are Hi(), Lo(), Cl(), Ad() for high, low, close and adjusted close prices

We can wrap the quantmod::getSymbols() function with these functions to retrieve only a specific column of data (or use %>%), which can be useful when extracting the price data of multiple stocks.

3.3 Retrieve Multiple Stocks

To retrieve multiple stocks in one go, we can use the quantmod::getSymbols() function in a for-loop like so:

# Tickers for Nvidia, Procter & Gamble, Mastercard, Walt Disney and Costco
tickers <- c("NVDA", "PG", "MA", "DIS", "COST")

startdate <- as.Date("2012-01-01")
enddate <- as.Date("2022-07-01")

price_data <- NULL

# Retrieve only the Adjusted Closing Price column
for(t in tickers) {
  price_data <- cbind(price_data,
                      quantmod::getSymbols(Symbols = t, src = "yahoo", auto.assign = FALSE,
                                           from = startdate, to = enddate, periodicity = "daily")) %>% Ad()
}

head(price_data)
##            NVDA.Adjusted PG.Adjusted MA.Adjusted DIS.Adjusted COST.Adjusted
## 2012-01-03      3.223092    48.56105    34.69548     34.30062      64.25737
## 2012-01-04      3.259822    48.53924    33.55394     34.78411      63.85479
## 2012-01-05      3.376901    48.33579    33.20625     35.36608      63.23959
## 2012-01-06      3.337875    48.21952    32.31204     35.73317      61.62176
## 2012-01-09      3.337875    48.42298    32.58529     35.58991      60.01151
## 2012-01-10      3.324100    48.19772    32.86890     35.48247      60.11784

4 PerformanceAnalytics Package

4.1 Calculating Returns

The discrete and log returns are the two common methods to calculate stock returns from its price. Discrete returns are calculated as \(R_t = \frac{P_t}{P_{t-1}} - 1\) and log returns are calculated as \(r_t = \log \bigg(\frac{P_t}{P_{t-1}} \bigg) = \log(P_t) - \log(P_{t-1})\). When returns are small, there is minor differences between the two methods. The difference is usually noticeable when we calculate returns of stocks using different periodicity of price data.

With the SPY data I had retrieved in Section 3, I would use the daily close and adjusted close price to calculate the returns. Adjusted closing price accounts for dividend data and thus can be used to approximate the dividend-adjusted return or total return of a stock. The weekly, monthly, etc. closing price is only to illustrate the differences in the return calculation as returns become larger.

In R, we can calculate them manually or use PerformanceAnalytics::Return.calculate() for simplicity.

# Discrete returns of spy_daily
spy_dailyR <- PerformanceAnalytics::Return.calculate(prices = Cl(spy_daily), method = "discrete")
head(spy_dailyR)
##                SPY.Close
## 2007-01-03            NA
## 2007-01-04  0.0021221123
## 2007-01-05 -0.0079763183
## 2007-01-08  0.0046250821
## 2007-01-09 -0.0008498831
## 2007-01-10  0.0033315799
# Remove NA from spy_dailyR
spy_dailyR <- na.omit(spy_dailyR)

# Check that only first observation was removed
dim(Cl(spy_daily)); dim(spy_dailyR)
## [1] 3923    1
## [1] 3922    1
# Check that the Return.calculate() is the same as using manual calculation
head(Cl(spy_daily) / stats::lag(Cl(spy_daily)) - 1)
##                SPY.Close
## 2007-01-03            NA
## 2007-01-04  0.0021221123
## 2007-01-05 -0.0079763183
## 2007-01-08  0.0046250821
## 2007-01-09 -0.0008498831
## 2007-01-10  0.0033315799
# Weekly, monthly, quarterly and yearly discrete returns to compare with log returns later
spy_weeklyR <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_weekly), method = "discrete"))

spy_monthlyR <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_monthly), method = "discrete"))

spy_quarterlyR <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_quarterly), method = "discrete"))

spy_yearlyR <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_yearly), method = "discrete"))
# Log returns of spy_daily
spy_dailylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_daily), method = "log"))
head(spy_dailylog)
##                SPY.Close
## 2007-01-04  0.0021198638
## 2007-01-05 -0.0080082993
## 2007-01-08  0.0046144192
## 2007-01-09 -0.0008502445
## 2007-01-10  0.0033260425
## 2007-01-11  0.0043708988
# Check that Return.calculate() is the same as using manual calculation
head(log(Cl(spy_daily) / stats::lag(Cl(spy_daily))))
##                SPY.Close
## 2007-01-03            NA
## 2007-01-04  0.0021198638
## 2007-01-05 -0.0080082993
## 2007-01-08  0.0046144192
## 2007-01-09 -0.0008502445
## 2007-01-10  0.0033260425
# Weekly, monthly, quarterly and yearly log returns
spy_weeklylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_weekly), method = "log"))

spy_monthlylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_monthly), method = "log"))

spy_quarterlylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_quarterly), method = "log"))

spy_yearlylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_yearly), method = "log"))
cbind(spy_dailyR, spy_dailylog) %>% `colnames<-`(c("Daily Discrete Return", "Daily Log Return")) %>% head()
##            Daily Discrete Return Daily Log Return
## 2007-01-04          0.0021221123     0.0021198638
## 2007-01-05         -0.0079763183    -0.0080082993
## 2007-01-08          0.0046250821     0.0046144192
## 2007-01-09         -0.0008498831    -0.0008502445
## 2007-01-10          0.0033315799     0.0033260425
## 2007-01-11          0.0043804651     0.0043708988
cbind(spy_weeklyR, spy_weeklylog) %>% `colnames<-`(c("Weekly Discrete Return", "Weekly Log Return")) %>% head()
##            Weekly Discrete Return Weekly Log Return
## 2007-01-12            0.019211699       0.019029484
## 2007-01-19           -0.002932128      -0.002936435
## 2007-01-26           -0.004831270      -0.004842978
## 2007-02-02            0.018855927       0.018680358
## 2007-02-09           -0.006007845      -0.006025965
## 2007-02-16            0.012435695       0.012359007
cbind(spy_monthlyR, spy_monthlylog) %>% `colnames<-`(c("Monthly Discrete Return", "Monthly Log Return")) %>% head()
##            Monthly Discrete Return Monthly Log Return
## 2007-02-28            -0.019617440       -0.019812416
## 2007-03-30             0.007592472        0.007563794
## 2007-04-30             0.044295725        0.043342711
## 2007-05-31             0.033920118        0.033357517
## 2007-06-29            -0.018849556       -0.019029473
## 2007-07-31            -0.031310192       -0.031810834
cbind(spy_quarterlyR, spy_quarterlylog) %>% `colnames<-`(c("Quarterly Discrete Return", "Quarterly Log Return")) %>% head()
##            Quarterly Discrete Return Quarterly Log Return
## 2007-06-29                0.05936615           0.05767076
## 2007-09-28                0.01429242           0.01419125
## 2007-12-31               -0.04174856          -0.04264507
## 2008-03-31               -0.09739420          -0.10246936
## 2008-06-30               -0.03023413          -0.03070061
## 2008-09-30               -0.09368655          -0.09837006
cbind(spy_yearlyR, spy_yearlylog) %>% `colnames<-`(c("Yearly Discrete Return", "Yearly Log Return")) %>% head()
##            Yearly Discrete Return Yearly Log Return
## 2008-12-31           -0.382805597       -0.48257123
## 2009-12-31            0.234929128        0.21101358
## 2010-12-31            0.128409886        0.12080946
## 2011-12-30           -0.001988072       -0.00199005
## 2012-12-31            0.134741068        0.12640449
## 2013-12-31            0.296889241        0.25996851
# Discrete returns using adjusted price
adjspy_dailyR <- na.omit(PerformanceAnalytics::Return.calculate(prices = Ad(spy_daily), method = "discrete"))

# Log returns using adjusted price
adjspy_dailylog <- na.omit(PerformanceAnalytics::Return.calculate(prices = Ad(spy_daily), method = "log"))

cbind(spy_dailyR, adjspy_dailyR, spy_dailylog, adjspy_dailylog) %>% 
  `colnames<-`(c("Discrete Return", "Discrete Adjusted Return", "Log Return", "Log Adjusted Return")) %>% 
  head()
##            Discrete Return Discrete Adjusted Return    Log Return
## 2007-01-04    0.0021221123             0.0021218657  0.0021198638
## 2007-01-05   -0.0079763183            -0.0079761167 -0.0080082993
## 2007-01-08    0.0046250821             0.0046248293  0.0046144192
## 2007-01-09   -0.0008498831            -0.0008499572 -0.0008502445
## 2007-01-10    0.0033315799             0.0033318429  0.0033260425
## 2007-01-11    0.0043804651             0.0043805809  0.0043708988
##            Log Adjusted Return
## 2007-01-04        0.0021196177
## 2007-01-05       -0.0080080961
## 2007-01-08        0.0046141677
## 2007-01-09       -0.0008503186
## 2007-01-10        0.0033263046
## 2007-01-11        0.0043710141

There is very small differences between the returns calculated using closing prices and adjusted closing prices, but when we calculate the cumulative returns later, there would be a huge difference.

4.2 Distribution of Returns

The distribution of returns may be useful when dealing with papers or work related to the non-normality of financial returns.

Use PerformanceAnalytics::chart.Histogram() to view the histogram of monthly discrete returns:

# Chart distribution of returns
PerformanceAnalytics::chart.Histogram(R = spy_monthlyR, 
                                      main = "Distribution of SPY Monthly Returns")

# Chart density of returns and normal distribution
PerformanceAnalytics::chart.Histogram(R = spy_monthlyR, 
                                      main = "Density Plot of SPY Monthly Returns", 
                                      methods = c("add.density", "add.normal"))

# Chart distribution of returns with VaR and Modified VaR risk metrics
PerformanceAnalytics::chart.Histogram(R = spy_monthlyR,
                                      main = "VaR and Modified VaR", 
                                      methods = "add.risk")

# Chart distribution of returns with Q-Q plot
PerformanceAnalytics::chart.Histogram(R = spy_monthlyR, 
                                      main = "Return Distribution with Q-Q plot", 
                                      methods = "add.qqplot")

Use PerformanceAnalytics::table.Distributions() to obtain distribution statistics, such as standard deviation, skewness and kurtosis:

# Table of distribution statistics
PerformanceAnalytics::table.Distributions(R = spy_monthlyR) %>% `colnames<-`("SPY Monthly Returns")
# PerformanceAnalytics package has individual functions for these statistics
# E.g. StdDev(), skewness(), kurtosis() can have different methods of calculation

4.3 Plot Cumulative Returns

The calculation of cumulative returns for discrete and log returns are different. Geometric chaining tend to be used with discrete returns, while arithmetic chaining is used with log returns.

Geometric chaining takes into account the effects of compounding over time, and the \(k\)-period returns is \((1+R) = (1+R_1) \times (1+R_2) \times (1+R_3) \times \dotsc \times (1+R_k)\). Arithmetic chaining simply sums the returns and is best used with log returns as log returns can be added across time. The \(k\)-period returns using arithmetic chaining is \(r = r_1 + r_2 + r_3 + \dotsc + r_k\). Although it is easy to find the cumulative returns of an asset with log returns, it is usually not used when calculating portfolio returns as it cannot be added across assets.

Chart cumulative daily discrete returns of SPY using arithmetic and geometric chaining with PerformanceAnalytics::chart.CumReturns():

par(mfrow = c(2,1))

PerformanceAnalytics::chart.CumReturns(R = spy_dailyR, geometric = FALSE, main = "Arithmetic Chaining")

PerformanceAnalytics::chart.CumReturns(R = spy_dailyR, geometric = TRUE, main = "Geometric Chaining")

To understand further about what is going on with the formula, I calculated the cumulative returns using base::cumsum() for arithmetic chaining and base::cumprod() for geometric chaining:

# Arithmetic chaining is simply the cumulative sum of returns
cs_spyR <- base::cumsum(x = spy_dailyR)

# Geometric chaining is the cumulative product of returns
cp_spyR <- base::cumprod(x = spy_dailyR + 1) - 1

cbind(cs_spyR, cp_spyR) %>% 
  `colnames<-`(c("Cumulative Sum", "Cumulative Product")) %>% head()
##            Cumulative Sum Cumulative Product
## 2007-01-04    0.002122112        0.002122112
## 2007-01-05   -0.005854206       -0.005871133
## 2007-01-08   -0.001229124       -0.001273205
## 2007-01-09   -0.002079007       -0.002122006
## 2007-01-10    0.001252573        0.001202504
## 2007-01-11    0.005633038        0.005588237

The plot of the cumulative sum and cumulative product of returns is the same as the arithmetic and geometric chaining of returns respectively:

par(mfrow = c(2,1))

plot(cs_spyR, main = "Cumulative Sum of SPY Daily Returns")

plot(cp_spyR, main = "Cumulative Product of SPY Daily Returns")

Digressing from the main purpose of this project for a while, I wished to show that using closing prices and adjusted closing prices can have very different effects on portfolio analysis. Using the SPY prices as a simple example, I had calculated the returns from both closing and adjusted closing price in Section 4.1.

Plot the cumulative returns of the closing and adjusted closing price:

spy_returns <- cbind(spy_dailyR, adjspy_dailyR) %>% `colnames<-`(c("Non-Adjusted", "Adjusted"))

PerformanceAnalytics::charts.PerformanceSummary(R = spy_returns, 
                                                geometric = TRUE,
                                                main = "Adjusted vs Non-Adjusted Returns")

Therefore, choosing whether to use the adjusted or non-adjusted closing prices to calculate returns can have an effect on portfolio analysis.

4.4 Plot Drawdowns

The drawdown measures the percentage decline in value of an investment from a peak to a trough. The recovery from the trough to its previous peak can also be an important consideration.

Use PerformanceAnalytics::chart.Drawdown() to plot the drawdown of SPY:

PerformanceAnalytics::chart.Drawdown(R = spy_dailyR, geometric = TRUE, main = "Drawdown of SPY")

The drawdown chart by itself may sometimes be insufficient, and adding the cumulative return chart may be wanted. PerformanceAnalytics::charts.PerformanceSummary() returns both cumulative return and drawdown charts:

PerformanceAnalytics::charts.PerformanceSummary(R = spy_dailyR, geometric = TRUE, main = "SPY Performance")

The PerformanceAnalytics::table.Drawdowns() returns the summary of drawdown statistics in a table:

# Show the top 5 drawdown of SPY
PerformanceAnalytics::table.Drawdowns(R = spy_dailyR, top = 5, geometric = TRUE)

4.5 For Multiple Stocks

In this section, I explored functions that are used for analyzing multi-stock portfolios.

4.5.1 Calculating Portfolio Returns (Basic)

PerformanceAnalytics::Return.portfolio() allows us to calculate the returns of portfolios based on the individual asset returns and the weights of the assets in the portfolio. To understand how these functions work, I used the verbose mode which returns a list of intermediary calculations. Calling ?Return.portfolio shows what is returned when verbose = TRUE:

  • returns: The portfolio returns.

  • contribution: The per period contribution to portfolio return of each asset. Contribution is calculated as BOP weight times the period’s return divided by BOP value. Period contributions are summed across the individual assets to calculate portfolio return

  • BOP.Weight: Beginning of Period (BOP) Weight for each asset. An asset’s BOP weight is calculated using the input weights (or assumed weights, see below) and rebalancing parameters given. The next period’s BOP weight is either the EOP weights from the prior period or input weights given on a rebalance period.

  • EOP.Weight: End of Period (BOP) Weight for each asset. An asset’s EOP weight is the sum of the asset’s BOP weight and contribution for the period divided by the sum of the contributions and initial weights for the portfolio.

  • BOP.Value: BOP Value for each asset. The BOP value for each asset is the asset’s EOP value from the prior period, unless there is a rebalance event. If there is a rebalance event, the BOP value of the asset is the rebalance weight times the EOP value of the portfolio. That effectively provides a zero-transaction cost change to the position values as of that date to reflect the rebalance. Note that the sum of the BOP values of the assets is the same as the prior period’s EOP portfolio value.

  • EOP.Value: EOP Value for each asset. The EOP value is for each asset is calculated as (1 + asset return) times the asset’s BOP value. The EOP portfolio value is the sum of EOP value across assets.

  • To calculate BOP and EOP position value, we create an index for each position. The sum of that value across assets represents an indexed value of the total portfolio. Note that BOP and EOP position values are only computed when geometric = TRUE.

If weights are not supplied, the function assumes an equal-weight portfolio:

# Calculate discrete returns of stocks in price_data
return_data <- na.omit(PerformanceAnalytics::Return.calculate(prices = price_data, method = "discrete"))

# Do not supply weight and set geometric = TRUE for this example
port_return1 <- PerformanceAnalytics::Return.portfolio(R = return_data, 
                                                       weights = NULL, 
                                                       geometric = TRUE, 
                                                       verbose = TRUE)

# Portfolio returns
data.frame(port_return1$returns)
# Contribution of individual asset to portfolio returns
data.frame(port_return1$contribution)
# Beginning of period weights
data.frame(port_return1$BOP.Weight)
# End of period weights
data.frame(port_return1$EOP.Weight)
# Beginning of period value
data.frame(port_return1$BOP.Value)
# End of period value
data.frame(port_return1$EOP.Value)

We can see that the BOP.Weight and BOP.Value is basically the previous period EOP.Weight and EOP.Value, except for the first row, when we started the portfolio with equal weights, and therefore we have equal portfolio value distributed across assets.

If we set geometric = FALSE:

# Set geometric = FALSE for this example
port_return2 <- PerformanceAnalytics::Return.portfolio(R = return_data, 
                                                       weights = NULL, 
                                                       geometric = FALSE, 
                                                       verbose = TRUE)

# Portfolio returns
data.frame(port_return2$returns)
# Contribution of individual asset to portfolio returns
data.frame(port_return2$contribution)
# Beginning of period weights
data.frame(port_return2$BOP.Weight)
# End of period weights
data.frame(port_return2$EOP.Weight)

The BOP.Weight is constant at 0.2 and the returns are different. The returns when geometric = FALSE should be the weighted average of the asset returns based on the starting weights.

cbind(rowSums(return_data * 0.2), port_return2$returns) %>% 
  `colnames<-`(c("Manual Calculation", "From Function")) %>% head()
##            Manual Calculation From Function
## 2012-01-04      -0.0028248686 -0.0028248686
## 2012-01-05       0.0056917074  0.0056917074
## 2012-01-06      -0.0112187648 -0.0112187648
## 2012-01-09      -0.0034928867 -0.0034928867
## 2012-01-10      -0.0002643813 -0.0002643813
## 2012-01-11      -0.0127559252 -0.0127559252

Since I only used the package to learn and perform simple portfolio analysis, I had not encountered a situation where I would set geometric = FALSE to calculate portfolio returns. One hypothetical case of using this may be to calculate the performance of a portfolio that is bought and sold on the same day (or week or any period), but why one would calculate that is beyond me.

4.5.2 Calculating Portfolio Returns (Advanced)

In the PerformanceAnalytics::Return.portfolio() function, we can state a vector or time series of weights and a rebalancing frequency of either “days”, “weeks”, “months”, “quarters” or “years”. If a rebalancing frequency was not supplied, a buy and hold strategy is assumed. If a time series of weights is supplied, the rebalancing frequency is ignored by the function.

Let’s assume a portfolio of 30% NVDA, 20% PG, 15% MA, 15% DIS and 20% COST and is rebalanced weekly (this is for the purpose of simplifying the example and does not necessarily follow practical strategies):

# State vector of weights in order of their columns in return_data
w <- c(0.3, 0.2, 0.15, 0.15, 0.2)

# Monthly rebalancing 
wk_rebal <- PerformanceAnalytics::Return.portfolio(R = return_data,
                                                   weights = w,
                                                   geometric = TRUE,
                                                   rebalance_on = "weeks", 
                                                   verbose = TRUE)

# Portfolio returns
data.frame(wk_rebal$returns)
# Contribution of individual asset to portfolio returns
data.frame(wk_rebal$contribution)
# Beginning of period weights
data.frame(wk_rebal$BOP.Weight)
# End of period weights
data.frame(wk_rebal$EOP.Weight)

Under BOP.Weight, the starting weights were exactly what I had supplied to the weights argument, and at the beginning of each week, the weights were rebalanced to the starting weights. 2012-01-04 was a Wednesday and the weights were rebalanced on 2012-01-09, which was on a Monday and the pattern would repeat till the week of the last observation.

If I supplied a time series of weights:

# Starting weights on 2012-01-04 and change weights on 2012-01-06
# Need to -1 from dates because rebalanced weights take effect on next day 
# Think of it as setting the EOP weights
ts_w <- xts(x = rbind(w, c(0.2, 0.3, 0.15, 0.2, 0.15)),
            order.by = index(return_data[c(1,3),]) - 1)

ts_w
##            [,1] [,2] [,3] [,4] [,5]
## 2012-01-03  0.3  0.2 0.15 0.15 0.20
## 2012-01-05  0.2  0.3 0.15 0.20 0.15
rebal_ts_w <- PerformanceAnalytics::Return.portfolio(R = return_data, 
                                                     weights = ts_w,
                                                     geometric = TRUE, 
                                                     rebalance_on = "weeks",
                                                     verbose = TRUE)

# Beginning of period weights
data.frame(rebal_ts_w$BOP.Weight)
# End of period weights
data.frame(rebal_ts_w$EOP.Weight)

The weekly rebalancing effect was ignored since time_w is a xts object.

4.5.3 Plot Weights

Chart changes in weights over time on portfolios with and without periodic rebalancing:

# Plot weights of portfolio that is not rebalanced frequently
plot(port_return1$BOP.Weight, legend.loc = "topleft")

# Plot weights of portfolio rebalanced weekly
plot(wk_rebal$BOP.Weight, legend.loc = "left")

4.5.4 Plot Portfolio Performance

Plotting the portfolio performance with the SPY as benchmark:

# Subset SPY returns such that it starts and ends on the same periods as the portfolio
spy_benchmark <- adjspy_dailyR[paste(as.character(start(return_data)), "/", as.character(end(return_data)), sep = ""),]

# Merge returns of portfolio with and without rebalancing and the spy_benchmark
return_comp <- cbind(port_return1$returns, wk_rebal$returns, spy_benchmark) %>%
  `colnames<-`(c("No Rebalancing", "Weekly Rebalancing", "SPY"))

PerformanceAnalytics::charts.PerformanceSummary(R = return_comp, 
                                                main = "Performance of Portfolios Against Benchmark", 
                                                geometric = TRUE, 
                                                legend.loc = "topleft")

PerformanceAnalytics::charts.PerformanceSummary() can be used to plot the performance of multiple portfolios. However, this can become confusing when too many portfolios are plotted. In that case, I prefer to add the argument plot.engine = "plotly" to use the plotly style of charts and separate the cumulative return and drawdown plots. For example, I would plot the cumulative return as such:

PerformanceAnalytics::chart.CumReturns(R = return_comp, 
                                       main = "Cumulative Return Using Plotly", 
                                       geometric = TRUE, 
                                       legend.loc = "topleft",
                                       plot.engine = "plotly")

4.5.5 Tables of Portfolio Performance

The PerformanceAnalytics package provides many useful functions to summarize the performance of portfolios in a table.

Use PerformanceAnalytics::table.AnnualizedReturns() to find annualized returns, standard deviation and Sharpe Ratio:

# scale = 252 because returns are in daily periodicity. Use 52 for weekly, 12 for monthly, 4 for quarterly
# Input risk-free rate to calculate Sharpe Ratio, which should be in the same periodicity as returns
PerformanceAnalytics::table.AnnualizedReturns(R = return_comp, scale = 252, Rf = 0.03/252, geometric = TRUE)

Use PerformanceAnalytics::table.CAPM() to find CAPM measures:

# Use SPY as benchmark (Rb)
PerformanceAnalytics::table.CAPM(Ra = return_comp[,1:2], Rb = return_comp[,3], scale = 252, Rf = 0.03/252)

Use PerformanceAnalytics::table.DownsideRisk() to find downside risk measures:

# Input Minimum Acceptable Rate (MAR) to calculate downside deviation
PerformanceAnalytics::table.DownsideRisk(R = return_comp, scale = 252, Rf = 0.03/252, MAR = 0.07/252)

Use PerformanceAnalytics::table.Stats() to find statistics such as arithmetic and geometric mean of returns and variance:

PerformanceAnalytics::table.Stats(R = return_comp)

5 Summary

It is important to know what setting geometric = TRUE does in the calculation of portfolio returns, when charting or other functions that require geometric chaining of returns. Using the wrong settings can have some impacts on the analysis of stock or portfolio performance. There are also separate functions in the PerformanceAnalytics package that calculates different risk measures, such as Value-at-Risk, Expected Tail Loss (or Expected Shortfall) and Standard Deviation, which I did not explicitly cover.

I hope that this basic guide to PerformanceAnalytics package can help those who are new to the package or want to know what the different arguments in the function does.