Investment Management with Python - 1

Fundamentals of Return and Risk
investment management

October 24, 2020

Fundamentals of returns

Price Return

The price return on an asset over some period, between time \(t\) and \(t+1\) is given by \(R_{t,t+1} = \frac{P_{t+1}- P_t}{P_t}\).

Code for getting returns from prices

import pandas as pd

def returns1(prices):
    return prices.pct_change()

def returns2(prices):
    return prices/prices.shift(1) - 1

def returns3(prices):
    return prices.iloc[1:].values/prices.iloc[:-1] - 1

Total Return

If the asset pays a dividend \(d\) during the time period \(t\) to \(t+1\) the total return is given by \(TR_{t,t+1} =\frac{P_{t+1} + d - P_t}{P_t}\).

Multiperiod Return

If \(R_1\) is the return in the time period \(t\) to \(t+1\) and \(R_2\) is the return for the time period \(t+1\) to \(t+2\). The total compounded return over the period \(t\) to \(t+2\) is given by \((1+R_1)(1+R_2)\).

Comparing return across time periods

Returns across different time periods can be compared using a process called annualization.

If \(R\) is the return for a given period and there are \(n\) such periods in a year then the annualized return is given by \((1+R)^{n}\).

If \(R_1,R_2,\dots R_n\) are the returns over \(n\) months and there are \(p\) such time periods in a year, the annualized return \(R\) is given by

\[ \begin{align*} (1+R)^{n/p} &= (1+R_1)(1+R_2) \cdots (1+R_n) \\ \implies R &= \left((1+R_1)(1+R_2) \cdots (1+R_n)\right)^{p/n} - 1 \end{align*} \]

Code for calculating Annualized return

def annualize_rets(r, periods_per_year):
    compounded_growth = (1+r).prod()
    n_periods = r.shape[0]
    return compounded_growth**(periods_per_year/n_periods)-1

Measures of Volatility

Standard deviation

If \(R\) is the random variable of the returns, the volatility is given by \(SD(R) = \sigma = \sqrt{Var(R)}\).

If \(R_1, R_2, \dots R_n\) are a series of returns, then the volatility is given by the standard deviation of the returns

\[ Volatility = \sqrt{\frac{\sum_{i=1}^n (R_i-\overline{R})^2}{n-1}} \]

Here we are computing standard deviation assuming the returns are a sample from an unknown distribution.

Code for calculating volatility

def volatility(returns):
    return returns.std()

Annualized volatility

If there are \(p\) periods in a year and the volatility during a given period is given by \(SD(R)\). The annualized volatility is given by \(SD(pR) = \sqrt(p)SD(R) = \sqrt{p}\sigma\).

Code for calculating Annualized volatility

def annualize_vol(r, periods_per_year):
    return r.std()*(periods_per_year**0.5)

Comparing returns with different volatility

The Sharpe ratio is the excess return per unit of volatility and is given by $ $.

Code for calculating Sharpe ratio

def sharpe_ratio(monthly_returns, riskfree_rate):
    annualized_vol = monthly_returns.std()*np.sqrt(12)
    n_months = len(monthly_returns)
    annualized_return = (monthly_returns+1).prod()**(12/n_months) - 1
    return     (annualized_return - riskfree_rate)/annualized_vol


A very popular measure of risk is called the maximum drawdown. Maximum drawdown is the maximum loss that you could have experienced, it is the worst return of the peak to trough that you could have experienced over the time that the return series are being analyzed.

Algorithm for computing drawdowns

  1. Convert the time series of returns to a time series that represents a wealth index. A wealth index is just the current amount of wealth what would have happened if I had taken let’s say a dollar or a \$1,000, and invested it over time. Just buy and hold. Just kept it in that asset all the way through that period.
  2. Compute a time series of the previous peaks.
  3. Compute the Drawdown as the difference between the previous peak and the current value.
  4. Compute the maximum of all drawdowns.

Code for computing drawdowns

def drawdown(return_series: pd.Series):
    """Takes a time series of asset returns.
       returns a DataFrame with columns for
       the wealth index, 
       the previous peaks, and 
       the percentage drawdown
    wealth_index = 1000*(1+return_series).cumprod()
    previous_peaks = wealth_index.cummax()
    drawdowns = (wealth_index - previous_peaks)/previous_peaks
    return pd.DataFrame({"Wealth": wealth_index,
                         "Previous Peak": previous_peaks,
                         "Drawdown": drawdowns})

max_drawdown = drawdown(return_series)["Drawdown"].max()

Problems with drawdowns

Max Drawdown is essentially dependent on two data points, and you don’t want to use statistics that are dependent on essentially two data points.

The other problem to watch out for in terms of drawdowns calculation is, drawdown on a daily basis is very different from drawdown on a weekly basis. If you look at drawdown on a daily basis, you’re going to see the worst worst-case. If you look at it on a weekly basis, the worst-case would have essentially disappeared because you are only looking at weekly data. If you read monthly data, it’s even less. So it’s very very sensitive to the granularity of the data.

Calmar ratio

The Calmar ratio is a risk adjusted return measure where the average annual rate of return for the last 36 months divided by the maximum drawdown for the last 36 months.

Back to top