590 Portfolio Performance Analysis & Benchmarking

Hao Zhang, University of Washington

1. Portfolio Initialization

Select tickers, get prices, choose numbers of shares to purchase

In [1]:
options(warn=-1)
options("getSymbols.warning4.0"=FALSE)
In [2]:
# 1.
getPrices <- function(tickers, 
                      as.of.dates=seq(from=as.Date("2007-01-01"), 
                                      to=Sys.Date(), 
                                      by="day"),
                      src="yahoo",
                      api.key=NULL){
  # Get closing prices for a vector of tickers
  # Catch the special ticker "CASHX" and handle carefully
  # Args:
  #   tickers: character vector of ticker symbols
  #   as.of.dates: date, or a vector of dates
  #   src: "yahoo", "av", or "tiingo"
  #   api.key: character string required if src="av" or src="tiingo"
  # Returns:
  #   xts object of closing prices at the given dates
  # Depends on:
  #   getSymbols and Cl functions from package 'quantmod'
  #   if src="av" or src="tiingo", then package 'jsonlite' is also required
  
  print("Getting prices...")
  
  myPrices <- NULL
  for (tkr in tickers) {
    print(tkr)
    if (tkr != "CASHX") {
      # HACK! getSymbols appears to have a bug when src="yahoo" is used.
      # The argument "to" evidently must be one day later than the last desired day!
      tmp <- getSymbols(
        tkr, 
        from=as.of.dates[1], 
        to=(as.Date(tail(as.of.dates, 1)) + 1), 
        src=src,
        api.key=api.key,
        return.class="xts",
        auto.assign=FALSE)
      tmp <- tmp[as.of.dates, ]  # Extract desired dates
      tmp <- Cl(tmp)  # Extract closing prices only
      index(tmp) <- as.Date(index(tmp)) # To fix a wierdness when using src="tiingo"
    }
    else {  # Add a column for CASHX
      tmp <- as.xts(rep(1.0, length(as.of.dates)), as.Date(as.of.dates))
    }
    myPrices <- cbind(myPrices, tmp)
  }#end for
  
  colnames(myPrices) <- tickers
  return(myPrices)
}#end getPrices
In [3]:
library(quantmod)
myTickers <- c("WMT", "COST", "TGT", "M", "WDAY", "XPO", "GPN", "CMCSA", "DFS", "T", "DPZ", 
               "V", "AWK", "MRK", "ADP", "ATUS", "L", "ORCL", "PVH", "ATVI", "CASHX")
as.of.dates <- c("2020-03-27", "2020-04-03")
myPrices <- getPrices(myTickers, as.of.dates)
print(myPrices)
Loading required package: xts

Loading required package: zoo


Attaching package: ‘zoo’


The following objects are masked from ‘package:base’:

    as.Date, as.Date.numeric


Loading required package: TTR

Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 

Version 0.4-0 included new data defaults. See ?getSymbols.

[1] "Getting prices..."
[1] "WMT"
[1] "COST"
[1] "TGT"
[1] "M"
[1] "WDAY"
[1] "XPO"
[1] "GPN"
[1] "CMCSA"
[1] "DFS"
[1] "T"
[1] "DPZ"
[1] "V"
[1] "AWK"
[1] "MRK"
[1] "ADP"
[1] "ATUS"
[1] "L"
[1] "ORCL"
[1] "PVH"
[1] "ATVI"
[1] "CASHX"
              WMT   COST   TGT    M   WDAY   XPO    GPN CMCSA   DFS     T
2020-03-27 109.58 284.33 94.74 5.53 136.21 52.92 144.96 34.57 38.41 29.84
2020-04-03 119.48 288.65 92.57 4.81 116.08 44.29 126.30 33.95 28.53 27.46
              DPZ      V    AWK   MRK    ADP  ATUS    L  ORCL   PVH  ATVI CASHX
2020-03-27 338.74 161.56 120.77 71.73 131.38 22.44 33.7 49.83 38.00 56.96     1
2020-04-03 328.23 151.85 113.32 76.25 128.57 21.91 32.7 49.40 30.99 59.98     1

Simply put the maximum 5% in cash reserve, and equally distribute the rest funds to each of the 20 stocks.

In [4]:
((1e6*0.95)/20)/myPrices['2020-03-27']
                WMT     COST      TGT        M     WDAY      XPO      GPN
2020-03-27 433.4733 167.0594 501.3722 8589.512 348.7262 897.5813 327.6766
              CMCSA      DFS        T      DPZ        V      AWK      MRK
2020-03-27 1374.024 1236.657 1591.823 140.2255 294.0084 393.3096 662.2055
                ADP     ATUS        L    ORCL  PVH     ATVI CASHX
2020-03-27 361.5466 2116.756 1409.496 953.241 1250 833.9186 47500
In [5]:
shares = c(434, 167, 501, 8590, 349, 898, 328, 1374, 1237, 1592,
  140, 294, 393, 662, 362, 2117, 1409, 953, 1250, 834)
equityValue = sum(shares * head(as.numeric(myPrices['2020-03-27']), -1))
leftCash = 1e6 - equityValue
leftCash
49956.684135
In [6]:
myShares = c(shares, leftCash)
sum(myShares * myPrices['2020-03-27'])
1e+06
In [7]:
transCost = 0.0010*equityValue
transCost
950.043315865
In [8]:
cashReserve = leftCash - transCost
cashReserve
49006.640819135
In [9]:
myShares = c(shares, cashReserve)
myShares
  1. 434
  2. 167
  3. 501
  4. 8590
  5. 349
  6. 898
  7. 328
  8. 1374
  9. 1237
  10. 1592
  11. 140
  12. 294
  13. 393
  14. 662
  15. 362
  16. 2117
  17. 1409
  18. 953
  19. 1250
  20. 834
  21. 49006.640819135
In [10]:
totalValue = sum(myShares*myPrices['2020-03-27'])
totalValue
999049.956684135
In [11]:
writeHoldings <- function(account.name, as.of.date, tickers, num.shares){
  # Write holdings to a csv file named like "H-uwid-2020-03-27.csv"
  # Args:
  #   account.name: account identifier
  #   as.of.date: date of the holdings, ccyy-mm-dd format
  #   tickers: character vector of ticker symbols
  #   num.shares: numeric vector of numbers of shares
  # Returns:
  #   data frame containing the columns written to the file
  
  # Make a data frame
  holdings <- data.frame(
    ticker = tickers,
    num.shares = num.shares,
    stringsAsFactors = FALSE)
  
  # Compose the file name
  filename <- paste("H-", account.name, "-", as.Date(as.of.date), ".csv", sep="")
  
  # Write to a file
  write.csv(holdings, file=filename, row.names=FALSE)

  return(holdings)
}#end writeHoldings

# Example of use
uwid <- "hzcfrm"

aDate <- "2020-03-27"

tmp <- writeHoldings(uwid, aDate, myTickers, myShares)

# To save the data frame in your workspace, give it unique name
assign("holdings.0327", tmp)
In [12]:
holdings.0327
A data.frame: 21 × 2
tickernum.shares
<chr><dbl>
WMT 434.00
COST 167.00
TGT 501.00
M 8590.00
WDAY 349.00
XPO 898.00
GPN 328.00
CMCSA 1374.00
DFS 1237.00
T 1592.00
DPZ 140.00
V 294.00
AWK 393.00
MRK 662.00
ADP 362.00
ATUS 2117.00
L 1409.00
ORCL 953.00
PVH 1250.00
ATVI 834.00
CASHX49006.64
In [13]:
# Checking there are 20 equity securities
length(myTickers) - 1
20
In [14]:
# Checking the maximum weight in any secuirity is 20% and the maximum weight left in cash is 5%
print(mean(head(as.numeric(myShares*myPrices['2020-03-27']), -1) < 0.2*totalValue) == 1)
print(tail(as.numeric(myShares*myPrices['2020-03-27']), 1) < 0.05*totalValue)
[1] TRUE
[1] TRUE

2. Transition Period Analysis

In [15]:
# 2. Calculating transition period return
transReturn = ((sum(myShares * myPrices['2020-03-27'])/1e6) - 1) * 100
transReturn
-0.0950043315865012

3. Checking for Splits

In [16]:
getSplitfactors <- function(tkr, 
                            as.of.dates=seq(from=as.Date("2007-01-01"), 
                                            to=Sys.Date(), 
                                            by="day"),
                            src="yahoo",
                            api.key=NULL){
  # Get cumulative split factors between dates
  # Args:
  #   tkr: a character ticker symbol
  #   as.of.dates: a vector of two or more dates
  #   src: only "yahoo" is valid at present
  #   api.key: character string required if src="av" or src="tiingo" (not valid at present)
  # Returns:
  #   xts object of cumulative split factors between the given dates
  # Depends on:
  #   getSplits function from package 'quantmod'
  
  print(paste("Getting split factors for ticker", tkr, sep=" ")) 
  
  splitfactors <- 1
  for (t in 2:length(as.of.dates)){
    # HACK! getSplits appears to have a bug when src="yahoo" is used.
    # The argument 'to' evidently must be one day later than the last desired day!
    splits <- getSplits(tkr, 
                        from=(as.Date(as.of.dates[t - 1]) + 1), 
                        to=as.Date(as.of.dates[t]) + 1,
                        src=src,
                        api.key=api.key,
                        return.class="xts",
                        auto.assign=FALSE)
    splits.cum <- prod(splits)
    if (is.na(splits.cum)){
      splits.cum <- 1
    }
    splitfactors <- c(splitfactors, splits.cum)
  }#end for
  
  splitfactors.xts <- as.xts(splitfactors, as.Date(as.of.dates))
  colnames(splitfactors.xts) <- tkr
  return(splitfactors.xts)
}#end getSplitfactors
In [17]:
# With getSplitfactors, the dates are aligned
splitfactors <- NULL
for (tkr in myTickers){
  if (tkr != "CASHX"){
    # Get split factors
    tkrtmp <- getSplitfactors(tkr, as.of.dates)
    splitfactors <- cbind(splitfactors, tkrtmp)
  }
}
# Add a column for CASHX
colnames(tkrtmp) <- "CASHX"
tkrtmp[ , "CASHX"] <- 1.0
splitfactors <- cbind(splitfactors, tkrtmp)
print(splitfactors)
[1] "Getting split factors for ticker WMT"
[1] "Getting split factors for ticker COST"
[1] "Getting split factors for ticker TGT"
[1] "Getting split factors for ticker M"
[1] "Getting split factors for ticker WDAY"
[1] "Getting split factors for ticker XPO"
[1] "Getting split factors for ticker GPN"
[1] "Getting split factors for ticker CMCSA"
[1] "Getting split factors for ticker DFS"
[1] "Getting split factors for ticker T"
[1] "Getting split factors for ticker DPZ"
[1] "Getting split factors for ticker V"
[1] "Getting split factors for ticker AWK"
[1] "Getting split factors for ticker MRK"
[1] "Getting split factors for ticker ADP"
[1] "Getting split factors for ticker ATUS"
[1] "Getting split factors for ticker L"
[1] "Getting split factors for ticker ORCL"
[1] "Getting split factors for ticker PVH"
[1] "Getting split factors for ticker ATVI"
           WMT COST TGT M WDAY XPO GPN CMCSA DFS T DPZ V AWK MRK ADP ATUS L
2020-03-27   1    1   1 1    1   1   1     1   1 1   1 1   1   1   1    1 1
2020-04-03   1    1   1 1    1   1   1     1   1 1   1 1   1   1   1    1 1
           ORCL PVH ATVI CASHX
2020-03-27    1   1    1     1
2020-04-03    1   1    1     1

4. Checking for Dividends

In [18]:
getDivpershare <- function(tkr,
                           as.of.dates=seq(from=as.Date("2007-01-01"), 
                                           to=Sys.Date(), 
                                           by="day"),
                           src="yahoo",
                           api.key=NULL){
  # Get cumulative dividend per share between dates
  # Args:
  #   tkr: a character ticker symbol
  #   as.of.dates: a vector of two or more dates
  #   src: only "yahoo" is valid at present
  #   api.key: character string required if src="av" or src="tiingo" (not valid at present)
  # Returns:
  #   xts object of cumulative dividend per share between the given dates
  # Depends on:
  #   getDividends function from package 'quantmod'
  
  print(paste("Getting dividends per share for ticker", tkr, sep=" "))
  
  divps <- 0
  for (t in 2:length(as.of.dates)){
    # HACK! getDividends appears to have a bug when src="yahoo" is used.
    # The argument 'to' evidently must be one day later than the last desired day!
    # ALSO: Sometimes a result is returned for a date one day before the 'from' day!
    # To be safe, the 'from' day is advanced by two. 
    # If as.of.dates[t - 1] is a Friday, then as.Date(as.of.dates[t - 1]) + 2 is a Sunday.
    div <- getDividends(tkr, 
                        from=(as.Date(as.of.dates[t - 1]) + 2), 
                        to=(as.Date(as.of.dates[t]) + 1),
                        src=src,
                        api.key=api.key,
                        return.class="xts",
                        auto.assign=FALSE)
    div.cum <- sum(div)
    if (is.na(div.cum)){
      div.cum <- 0
    }
    divps <- c(divps, div.cum)
  }#end for
  
  divps.xts <- as.xts(divps, as.Date(as.of.dates))
  colnames(divps.xts) <- tkr
  return(divps.xts)
}#end getDivpershare
In [19]:
# With getDivpershare, the dates are aligned
divps <- NULL
for (tkr in myTickers){
  if (tkr != "CASHX"){
    # Get dividend per share
    tkrtmp <- getDivpershare(tkr, as.of.dates)
    divps <- cbind(divps, tkrtmp)
  }
}
# Add a column for CASHX
colnames(tkrtmp) <- "CASHX"
tkrtmp[ , "CASHX"] <- 0.0
divps <- cbind(divps, tkrtmp)
print(divps)
[1] "Getting dividends per share for ticker WMT"
[1] "Getting dividends per share for ticker COST"
[1] "Getting dividends per share for ticker TGT"
[1] "Getting dividends per share for ticker M"
[1] "Getting dividends per share for ticker WDAY"
[1] "Getting dividends per share for ticker XPO"
[1] "Getting dividends per share for ticker GPN"
[1] "Getting dividends per share for ticker CMCSA"
[1] "Getting dividends per share for ticker DFS"
[1] "Getting dividends per share for ticker T"
[1] "Getting dividends per share for ticker DPZ"
[1] "Getting dividends per share for ticker V"
[1] "Getting dividends per share for ticker AWK"
[1] "Getting dividends per share for ticker MRK"
[1] "Getting dividends per share for ticker ADP"
[1] "Getting dividends per share for ticker ATUS"
[1] "Getting dividends per share for ticker L"
[1] "Getting dividends per share for ticker ORCL"
[1] "Getting dividends per share for ticker PVH"
[1] "Getting dividends per share for ticker ATVI"
           WMT COST TGT M WDAY XPO GPN CMCSA DFS T DPZ V AWK MRK ADP ATUS L
2020-03-27   0    0   0 0    0   0   0  0.00   0 0   0 0   0   0   0    0 0
2020-04-03   0    0   0 0    0   0   0  0.23   0 0   0 0   0   0   0    0 0
           ORCL PVH ATVI CASHX
2020-03-27    0   0    0     0
2020-04-03    0   0    0     0
In [20]:
# Calculate the dividend amount for CMCSA
divCMCSA = as.numeric(divps['2020-04-03', 'CMCSA']*holdings.0327[holdings.0327$ticker=='CMCSA', 'num.shares'])
divCMCSA
316.02
In [21]:
# Account for the dividend amount by adjusting liquidty reserve
holdings.0327[holdings.0327$ticker=='CASHX', 'num.shares'] = holdings.0327[holdings.0327$ticker=='CASHX', 
                                                                           'num.shares']+divCMCSA
holdings.0327[holdings.0327$ticker=='CASHX', 'num.shares']
49322.660819135
In [22]:
# Updated holdings dataframe
holdings.0327
A data.frame: 21 × 2
tickernum.shares
<chr><dbl>
WMT 434.00
COST 167.00
TGT 501.00
M 8590.00
WDAY 349.00
XPO 898.00
GPN 328.00
CMCSA 1374.00
DFS 1237.00
T 1592.00
DPZ 140.00
V 294.00
AWK 393.00
MRK 662.00
ADP 362.00
ATUS 2117.00
L 1409.00
ORCL 953.00
PVH 1250.00
ATVI 834.00
CASHX49322.66
In [23]:
# Also modify the myShares
myShares[length(myShares)] = tail(myShares, 1) + divCMCSA
tail(myShares, 1)
49322.660819135

5. Calculating Weekly Management Fee

In [24]:
mgmtFee = (sum(myShares * myPrices['2020-04-03']))*0.0002
mgmtFee
188.978094774227
In [25]:
# Account for weekly management fee by adjusting liquidty reserve
holdings.0327[holdings.0327$ticker=='CASHX', 'num.shares'] = holdings.0327[holdings.0327$ticker=='CASHX', 
                                                                           'num.shares'] - mgmtFee 
holdings.0327[holdings.0327$ticker=='CASHX', 'num.shares']
49133.6827243608
In [26]:
holdings.0327
A data.frame: 21 × 2
tickernum.shares
<chr><dbl>
WMT 434.00
COST 167.00
TGT 501.00
M 8590.00
WDAY 349.00
XPO 898.00
GPN 328.00
CMCSA 1374.00
DFS 1237.00
T 1592.00
DPZ 140.00
V 294.00
AWK 393.00
MRK 662.00
ADP 362.00
ATUS 2117.00
L 1409.00
ORCL 953.00
PVH 1250.00
ATVI 834.00
CASHX49133.68
In [27]:
# Also modify myShares
myShares[length(myShares)] = tail(myShares, 1) - mgmtFee
tail(myShares, 1)
49133.6827243608

6. Making Two Trades

In [28]:
# Trade 1: Buying 100 shares of WMT
myShares[1] = head(myShares, 1) + 100
myShares[length(myShares)] = tail(myShares, 1) - 100*myPrices['2020-04-03', 'WMT']*(1 + 0.0010) 
myShares
  1. 534
  2. 167
  3. 501
  4. 8590
  5. 349
  6. 898
  7. 328
  8. 1374
  9. 1237
  10. 1592
  11. 140
  12. 294
  13. 393
  14. 662
  15. 362
  16. 2117
  17. 1409
  18. 953
  19. 1250
  20. 834
  21. 37173.7344240608
In [29]:
# Trade 2: Selling 1000 shares of M
myShares[4] = myShares[4] - 1000
myShares[length(myShares)] = tail(myShares, 1) + 1000*myPrices['2020-04-03', 'M']*(1 - 0.0010) 
myShares
  1. 534
  2. 167
  3. 501
  4. 7590
  5. 349
  6. 898
  7. 328
  8. 1374
  9. 1237
  10. 1592
  11. 140
  12. 294
  13. 393
  14. 662
  15. 362
  16. 2117
  17. 1409
  18. 953
  19. 1250
  20. 834
  21. 41978.9244240608
In [30]:
# Checking the maximum weight in any secuirity is 20% and the maximum weight left in cash is 5%
print(mean(head(as.numeric(myShares*myPrices['2020-04-03']), -1) < 0.2*sum(myShares*myPrices['2020-04-03'])) == 1)
print(tail(as.numeric(myShares*myPrices['2020-04-03']), 1) < 0.05*sum(myShares*myPrices['2020-04-03']))
[1] TRUE
[1] TRUE
In [31]:
# Write a holdings file
aDate <- "2020-04-03"
tmp <- writeHoldings(uwid, aDate, myTickers, myShares)
assign("holdings.0403", tmp)

7. Calculating Returns

In [32]:
# Caculating income return
iR = (divCMCSA/sum(holdings.0327$num.shares * myPrices['2020-03-27'])) * 100
iR
0.0316280299132343
In [33]:
# Calculating price return
pR = ((sum(holdings.0403$num.shares * myPrices['2020-04-03']) - divCMCSA)/
sum(holdings.0327$num.shares * myPrices['2020-03-27'])- 1) * 100
pR
-5.48534252596672
In [34]:
# Calculating total return
tR = (sum(holdings.0403$num.shares * myPrices['2020-04-03'])/
sum(holdings.0327$num.shares * myPrices['2020-03-27']) - 1) * 100
tR
-5.45371449605347

8. Writing A Summary Report

In [35]:
writeAccountSummary <- function(account.name, as.of.dates, deposits, withdrawals, 
                                dividends, fees, tc, values, 
                                income.returns, price.returns, total.returns) {
  # Write an account summary file in .csv format named like "A-uwid-2020-04-03.csv"
  # Args:
  #   account.name: account identifier
  #   as.of.dates: vector of dates, ccyy-mm-dd format
  #   deposits: numeric vector of deposit amounts
  #   withdrawals: numeric vector of withdrawal amounts
  #   dividends: numeric vector of dividend amounts
  #   fees: numeric vector of fee amounts
  #   tc: numeric vector of transaction cost amounts
  #   values: numeric vector of account values
  #   income.returns: numeric vector of period income returns between dates
  #   price.returns: numeric vector of period price returns between dates
  #   total.returns: numeric vector of period total returns between dates
  # Returns:
  #   data frame containing the columns written to the file

  numDates <- length(as.of.dates)
  
  # Make a data frame
  AccountSummary <- data.frame(
    account.name = rep(account.name, numDates),
    as.of.date = as.of.dates,
    deposits = deposits,
    withdrawals = withdrawals,
    dividends = dividends,
    fees = fees,
    tc = tc,
    value = values,
    income.return = income.returns,
    price.return = price.returns,
    total.return = total.returns,
    stringsAsFactors = FALSE)

  # Compose the file name
  filename <- paste("A-", account.name, "-", tail(as.of.dates, 1), ".csv", sep="")
  
  # Write to a file
  write.csv(AccountSummary, file=filename, row.names=FALSE)

  return(AccountSummary)
}#end writeAccountSummary

# Example of use
uwid <- "hzcfrm"
dates <- c("2020-03-27", "2020-04-03")
deposits <- c(1000000, 0)
withdrawals <- c(0, 0)
dividends <- c(0, divCMCSA)
fees <- c(0, mgmtFee)
tc <- c(transCost, (100*myPrices['2020-04-03', 'WMT'] + 1000*myPrices['2020-04-03', 'M'])*0.0010)
values <- c(sum(holdings.0327$num.shares * myPrices['2020-03-27']),
            sum(holdings.0403$num.shares * myPrices['2020-04-03']))
incomereturns <- c(0, iR)
pricereturns <- c(transReturn, pR)
totalreturns <- c(transReturn, tR)

tmp <- writeAccountSummary(uwid, dates, deposits, withdrawals, dividends, fees, tc, values,
                           incomereturns, pricereturns, totalreturns)

# To save the data frame in your workspace, give it unique name
assign('summary.0403', tmp)
In [36]:
summary.0403
A data.frame: 2 × 11
account.nameas.of.datedepositswithdrawalsdividendsfeestcvalueincome.returnprice.returntotal.return
<chr><chr><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl><dbl>
hzcfrm2020-03-271e+060 0.00 0.0000950.0433999177.00.00000000-0.09500433-0.09500433
hzcfrm2020-04-030e+000316.02188.9781 16.7580944684.70.03162803-5.48534253-5.45371450