5 min read

Investment Management with Python - 1

Table of Contents

Price Return

The price return on an asset over some period, between time tt and t+1t+1 is given by Rt,t+1=Pt+1βˆ’PtPtR_{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 dd during the time period tt to t+1t+1 the total return is given by TRt,t+1=Pt+1+dβˆ’PtPtTR_{t,t+1} =\frac{P_{t+1} + d - P_t}{P_t}.

Multiperiod Return

If R1R_1 is the return in the time period tt to t+1t+1 and R2R_2 is the return for the time period t+1t+1 to t+2t+2. The total compounded return over the period tt to t+2t+2 is given by (1+R1)(1+R2)(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 RR is the return for a given period and there are nn such periods in a year then the annualized return is given by (1+R)n(1+R)^{n}.

If R1,R2,…RnR_1,R_2,\dots R_n are the returns over nn months and there are pp such time periods in a year, the annualized return RR is given by

(1+R)n/p=(1+R1)(1+R2)β‹―(1+Rn)β€…β€ŠβŸΉβ€…β€ŠR=((1+R1)(1+R2)β‹―(1+Rn))p/nβˆ’1\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 RR is the random variable of the returns, the volatility is given by SD(R)=Οƒ=Var(R)SD(R) = \sigma = \sqrt{Var(R)}.

If R1,R2,…RnR_1, R_2, \dots R_n are a series of returns, then the volatility is given by the standard deviation of the returns

Volatility=βˆ‘i=1n(Riβˆ’Rβ€Ύ)2nβˆ’1Volatility = \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 pp periods in a year and the volatility during a given period is given by SD(R)SD(R). The annualized volatility is given by SD(pR)=(p)SD(R)=pσ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 E[Raβˆ’Rf]Οƒ \frac{\mathbb{E}[R_a - R_f]}{\sigma}.

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

Drawdowns

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.