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.
library(dplyr) # For data manipulation and piping function
library(PerformanceAnalytics) # For portfolio analysis and return calculation
library(quantmod) # For retrieving data
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.
Retrieve historical data of SPDR S&P500 ETF (SPY):
<- quantmod::getSymbols(Symbols = "SPY", src = "yahoo", auto.assign = FALSE)
spy_daily
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:
<- quantmod::getSymbols(Symbols = "IWM", src = "yahoo", auto.assign = FALSE,
iwm_monthly 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
<- xts::to.period(x = spy_daily, period = "weeks")
spy_weekly
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
<- xts::to.period(x = spy_daily, period = "months")
spy_monthly
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
<- xts::to.period(x = spy_daily, period = "quarters")
spy_quarterly
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
<- xts::to.period(x = spy_daily, period = "years")
spy_yearly
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.
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
"2013"] iwm_monthly[
## 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
"2013-10/2014-06"] iwm_monthly[
## 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
::Op(iwm_monthly) %>% head() quantmod
## 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
::Vo(iwm_monthly) %>% head() quantmod
## 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.
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
<- c("NVDA", "PG", "MA", "DIS", "COST")
tickers
<- as.Date("2012-01-01")
startdate <- as.Date("2022-07-01")
enddate
<- NULL
price_data
# Retrieve only the Adjusted Closing Price column
for(t in tickers) {
<- cbind(price_data,
price_data ::getSymbols(Symbols = t, src = "yahoo", auto.assign = FALSE,
quantmodfrom = 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
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
<- PerformanceAnalytics::Return.calculate(prices = Cl(spy_daily), method = "discrete")
spy_dailyR 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
<- na.omit(spy_dailyR)
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
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_weekly), method = "discrete"))
spy_weeklyR
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_monthly), method = "discrete"))
spy_monthlyR
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_quarterly), method = "discrete"))
spy_quarterlyR
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_yearly), method = "discrete")) spy_yearlyR
# Log returns of spy_daily
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_daily), method = "log"))
spy_dailylog 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
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_weekly), method = "log"))
spy_weeklylog
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_monthly), method = "log"))
spy_monthlylog
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_quarterly), method = "log"))
spy_quarterlylog
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Cl(spy_yearly), method = "log")) spy_yearlylog
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
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Ad(spy_daily), method = "discrete"))
adjspy_dailyR
# Log returns using adjusted price
<- na.omit(PerformanceAnalytics::Return.calculate(prices = Ad(spy_daily), method = "log"))
adjspy_dailylog
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.
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
::chart.Histogram(R = spy_monthlyR,
PerformanceAnalyticsmain = "Distribution of SPY Monthly Returns")
# Chart density of returns and normal distribution
::chart.Histogram(R = spy_monthlyR,
PerformanceAnalyticsmain = "Density Plot of SPY Monthly Returns",
methods = c("add.density", "add.normal"))
# Chart distribution of returns with VaR and Modified VaR risk metrics
::chart.Histogram(R = spy_monthlyR,
PerformanceAnalyticsmain = "VaR and Modified VaR",
methods = "add.risk")
# Chart distribution of returns with Q-Q plot
::chart.Histogram(R = spy_monthlyR,
PerformanceAnalyticsmain = "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
::table.Distributions(R = spy_monthlyR) %>% `colnames<-`("SPY Monthly Returns") PerformanceAnalytics
# PerformanceAnalytics package has individual functions for these statistics
# E.g. StdDev(), skewness(), kurtosis() can have different methods of calculation
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))
::chart.CumReturns(R = spy_dailyR, geometric = FALSE, main = "Arithmetic Chaining")
PerformanceAnalytics
::chart.CumReturns(R = spy_dailyR, geometric = TRUE, main = "Geometric Chaining") PerformanceAnalytics
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
<- base::cumsum(x = spy_dailyR)
cs_spyR
# Geometric chaining is the cumulative product of returns
<- base::cumprod(x = spy_dailyR + 1) - 1
cp_spyR
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:
<- cbind(spy_dailyR, adjspy_dailyR) %>% `colnames<-`(c("Non-Adjusted", "Adjusted"))
spy_returns
::charts.PerformanceSummary(R = spy_returns,
PerformanceAnalyticsgeometric = 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.
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:
::chart.Drawdown(R = spy_dailyR, geometric = TRUE, main = "Drawdown of SPY") PerformanceAnalytics
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:
::charts.PerformanceSummary(R = spy_dailyR, geometric = TRUE, main = "SPY Performance") PerformanceAnalytics
The PerformanceAnalytics::table.Drawdowns()
returns the
summary of drawdown statistics in a table:
# Show the top 5 drawdown of SPY
::table.Drawdowns(R = spy_dailyR, top = 5, geometric = TRUE) PerformanceAnalytics
In this section, I explored functions that are used for analyzing multi-stock portfolios.
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
<- na.omit(PerformanceAnalytics::Return.calculate(prices = price_data, method = "discrete"))
return_data
# Do not supply weight and set geometric = TRUE for this example
<- PerformanceAnalytics::Return.portfolio(R = return_data,
port_return1 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
<- PerformanceAnalytics::Return.portfolio(R = return_data,
port_return2 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.
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
<- c(0.3, 0.2, 0.15, 0.15, 0.2)
w
# Monthly rebalancing
<- PerformanceAnalytics::Return.portfolio(R = return_data,
wk_rebal 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
<- xts(x = rbind(w, c(0.2, 0.3, 0.15, 0.2, 0.15)),
ts_w 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
<- PerformanceAnalytics::Return.portfolio(R = return_data,
rebal_ts_w 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.
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")
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
<- adjspy_dailyR[paste(as.character(start(return_data)), "/", as.character(end(return_data)), sep = ""),]
spy_benchmark
# Merge returns of portfolio with and without rebalancing and the spy_benchmark
<- cbind(port_return1$returns, wk_rebal$returns, spy_benchmark) %>%
return_comp `colnames<-`(c("No Rebalancing", "Weekly Rebalancing", "SPY"))
::charts.PerformanceSummary(R = return_comp,
PerformanceAnalyticsmain = "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:
::chart.CumReturns(R = return_comp,
PerformanceAnalyticsmain = "Cumulative Return Using Plotly",
geometric = TRUE,
legend.loc = "topleft",
plot.engine = "plotly")
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
::table.AnnualizedReturns(R = return_comp, scale = 252, Rf = 0.03/252, geometric = TRUE) PerformanceAnalytics
Use PerformanceAnalytics::table.CAPM()
to find CAPM
measures:
# Use SPY as benchmark (Rb)
::table.CAPM(Ra = return_comp[,1:2], Rb = return_comp[,3], scale = 252, Rf = 0.03/252) PerformanceAnalytics
Use PerformanceAnalytics::table.DownsideRisk()
to find
downside risk measures:
# Input Minimum Acceptable Rate (MAR) to calculate downside deviation
::table.DownsideRisk(R = return_comp, scale = 252, Rf = 0.03/252, MAR = 0.07/252) PerformanceAnalytics
Use PerformanceAnalytics::table.Stats()
to find
statistics such as arithmetic and geometric mean of returns and
variance:
::table.Stats(R = return_comp) PerformanceAnalytics
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.