9.3. Expanding returns

Note

The code below might need to be modified to work as of Feb 2023. The fix is here.

9.3.1. The problem

You know the charts that show cumulative returns if you’d bought and held a stock since some long ago date? Let’s make one!

This is called “expanding returns” because you get the total returns from day 0 to day N, then from day 0 to day N+1, and so on; the window is expanding instead of having a fixed number of units or containing a specific increment of time.

9.3.2. Download the returns

We need a dataset with firm, date, and the daily return. Let’s build it:

#!pip install pandas_datareader # uncomment and run this ONE TIME ONLY to install pandas data reader
import pandas as pd
import numpy as np
import pandas_datareader as pdr # you might need to install this (see above)
from datetime import datetime
import yfinance as yf

# choose your firms and dates 
stocks = ['SBUX','AAPL','MSFT']
start  = datetime(1980, 1, 1)
end    = datetime(2022, 7, 31)

Tip

The code in the next block is explained more thoroughly in handouts/factor_loading_simple.ipynb in the textbook repo because that file prints the status of the data throughout. Looking at this might help.

# download stock prices 
# here, from yahoo: not my fav source, but quick. 
# we need to do some data manipulation to get the data ready 
stock_prices         = yf.download(stocks, start , end)
stock_prices.index   = stock_prices.index.tz_localize(None)      # change yf date format to match pdr
stock_prices         = stock_prices.filter(like='Adj Close')     # reduce to just columns with this in the name
stock_prices.columns = stock_prices.columns.get_level_values(1)  # tickers as col names, works no matter order of tics

# refmt from wide to long
stock_prices = stock_prices.stack().swaplevel().sort_index().reset_index()
stock_prices.columns = ['Firm','Date','Adj Close']

# add return var = pct_change() function compares to prior row
# EXCEPT: don't compare for first row of one firm with last row of prior firm!
# MAKE SURE YOU CREATE THE VARIABLES WITHIN EACH FIRM - use groupby
stock_prices['ret'] = stock_prices.groupby('Firm')['Adj Close'].pct_change()
stock_prices['ret'] = stock_prices['ret'] 
stock_prices.head(15)
Firm Date Adj Close ret
0 AAPL 1980-12-12 0.100040 NaN
1 AAPL 1980-12-15 0.094820 -0.052171
2 AAPL 1980-12-16 0.087861 -0.073398
3 AAPL 1980-12-17 0.090035 0.024751
4 AAPL 1980-12-18 0.092646 0.028992
5 AAPL 1980-12-19 0.098300 0.061029
6 AAPL 1980-12-22 0.103084 0.048670
7 AAPL 1980-12-23 0.107434 0.042199
8 AAPL 1980-12-24 0.113088 0.052628
9 AAPL 1980-12-26 0.123527 0.092310
10 AAPL 1980-12-29 0.125267 0.014083
11 AAPL 1980-12-30 0.122222 -0.024304
12 AAPL 1980-12-31 0.118743 -0.028468
13 AAPL 1981-01-02 0.120048 0.010988
14 AAPL 1981-01-05 0.117438 -0.021738

9.3.3. Getting the expanding returns

Notice that this dataset has the simple return for a period, not the gross returns (defined here).

To compute \(R_i[0,T]\) for all firms \(i\) and each time \(T\) in the dataset, you’re going to need to use groupby. You have two equivalent options from there:

  1. For each firm, get the cumprod() of the gross return over its time series.

    df.assign(R=1+df['r']).groupby('firm')['R'].cumprod()
    
  2. For each firm, take the product of \(1+r\) for all prior periods using the expanding window functionality.

    df.groupby('firm')['r'].expanding().apply(lambda x: np.prod(1+x))
    

Which you choose is up to you, but in my testing, the cumprod approach is 2.5x faster.

stock_prices['cumret'] = \
(
    stock_prices
    .assign(ret=1+stock_prices['ret'])
    .groupby('Firm')
    ['ret']
    .cumprod()
)

9.3.4. Plotting the total returns

If only we could turn back time.

(stock_prices.set_index('Date').groupby('Firm')['cumret']
 .plot(title="If you bought $1 back when, you'd have this now",
       figsize=(6,5))
);
../../_images/05a_expanding_9_0.png