9.1. Compounding returns

Warning

Do NOT use .mean() to cumulate or compound returns!!!!!

9.1.1. The math

Let \(r_t = (P_t+D_t)/P_{t-1}\) be the simple return for a single period with dividends included. E.g. \(r_t=0.05\).

Let \(R_t = (1+r_t)\) be the gross (thus, the capital “R”) return for a single period. E.g. \(R_t=1.05\).

The gross return for a number of periods is

\[ R[0,T] = \prod_{t=1}^T R_t \]

and the simple return for the same set of periods is simply the above minus 1: $\( r[0,T] = \left(\prod_{t=1}^T R_t\right) - 1 \)$

We can compute this in pandas pretty easily:

9.1.2. The code

Assume you have simple periodic (little \(r_t\)) returns in a variable called “ret” in df for various assets and you want to compound them to a longer period of time (e.g. monthly).

To adapt the code below to your use

  1. replace “df” with the name of your dataframe

  2. replace “asset” with the name of the variable(s) identifying the assets whose returns you want to compound

  3. replace “timeperiod” with the name of the variable(s) containing the measurement of time that you want to compound to, for example, a year variable

Here are two ways to accomplish the compounding:

(df
   # compute gross returns for each asset
   .assign(R = 1+df['ret'])
   # for each portfolio and time period...
   .groupby(['asset','timeperiod'])
   # get the gross returns, and cumulate by taking the product
   ['R'].prod()
   # subtract one to get back to simple returns
   -1
)
(df
 # for each asset and time period
 .groupby(["asset", "timeperiod"])
 # agg() does the function inside for each group (asset-timeperiod combo)
 # 1+x['ret'] gets gross return (R) for each observation in the data
 # (1+x{'ret']).prod() takes the product of all R for that group
 # and then subtract one
 .agg(ret=lambda x: ((1 + x["ret"]).prod()) - 1)
)

9.1.3. Optional: Log returns

Just as an FYI, there is another way to compound returns you might hear about in other contexts: using “log returns”.

A “log return” is \(ln(1+r)\) or \(ln(R)\), using the natural log. It’s the continuously compounded rate of return. If you earned a simple return of \(r=10\%\), one month, the continuously compounded rate of return over that month is \(ln(1+0.1)=9.5\%\).

Of course, no one speaks in terms of continuously compounding returns, but log returns has its uses.

Note

  1. You can not add the simple rates of returns across time to get the total compounded return: \(r_{[0,1]} +r_{[1,2]} \neq r_{[0,2]}\)

  2. But you can add log returns across time to get the correctly compounded total log return: \(ln(1+r_{[0,1]}) +ln(1+r_{[1,2]}) = ln(1+ r_{[0,2]})\)

Illustration: If returns in January (\(r_{[0,1]}\)) are 10%, and also 10% in February (\(r_{[1,2]}\)), your total return for both months (\(r_{[0,2]}\)) is \((1+r_{[0,1]})(1+r_{[0,2]})=21\%\).

Additionally, \(ln(1.1) +ln(1.1) = ln(1.21)=0.1906\).

This second observation makes it easy to compound returns in large datasets another way than what I showed above. Taking advantage of the fact that \(e(ln(x))=x\) (in step 2 below) and \(e^a*e^b*e^c=e^{a+b+c}\) (in step 3 below), we can rewrite \(R[0,T]\) as:

\[ R[0,T] = \prod_{t=1}^T R_t = \prod_{t=1}^T ( e(ln( R_t)) ) =e(\sum_{t=1}^T ln(R_t))\]

Thus, to compound returns, you can sum the log returns. Then to convert the compounded log return back to a simple return, just exponentiate and subtract 1.

The code looks like this:

(df
 .assign(logR = np.log(1+df.ret))
 # for each asset and time period
 .groupby(["asset", "timeperiod"])
 # sum log returns
 ['logR'].sum()
 # exponentiate and subtract 1
 .assign(ret = binlambda x: np.exp(x['logR'])-1
)

Warning

You can add log returns for one asset over time, but you can’t combine log returns across assets at the time time.