Sharpe, Sortino and Calmar Ratios with Python

by John | October 17, 2020


Join the discussion

Share this post with your friends!


In this article we will calculate the a number of well know statistics related to risk and reward in equities. In order to provide examples on real data we will use the following stocks to illustrate the concepts shown. Since the statistics in question are usually calculated on a portfolio, we will add an equal weighted portfolio to the analysis also. 

 

  • Apple: Stock ticker = AAPL
  • Amazon: Stock ticker = AMZN
  • Facebook: Stock ticker = FB
  • Google: Stock ticker = GOOGL
  • Microsoft: Stock ticker = MSFT
  • Port: Equally weighted portfolio of the securities above. 

 

Statistics to be calculated

  • Sharpe ratio 
  • Sortino Ratio 
  • Max Drawdown
  • Calmar Ratio 

 

Getting the Data

 

In order to get the data necessary to complete this analysis we will make use of Pandas Datareader, which allows us to directly download stock data into Python. Execute the following code block in your editor:

import pandas_datareader.data as web
import datetime as dt
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('ggplot')


start = dt.datetime(2013, 1, 1)
end = dt.datetime(2020, 10, 1)

tickers = ['AAPL', 'AMZN', 'MSFT', 'GOOGL','FB']

stocks = web.DataReader(tickers,
                        'yahoo', start, end)['Adj Close']

stocks.head()

 

Out:
Symbols          AAPL        AMZN       MSFT       GOOGL         FB
Date                                                               
2013-01-02  17.094694  257.309998  23.241472  361.987000  28.000000
2013-01-03  16.878920  258.480011  22.930120  362.197205  27.770000
2013-01-04  16.408764  259.149994  22.500971  369.354340  28.760000
2013-01-07  16.312239  268.459991  22.458902  367.742737  29.420000
2013-01-08  16.356150  266.380005  22.341091  367.017029  29.059999

 

Change the dataframe to percentage change and create a column for the equally weighted portfolio. Plot the normalized stock prices for comparison. 

df = stocks.pct_change().dropna()
df['Port'] = df.mean(axis=1) # 20% apple, ... , 20% facebook
(df+1).cumprod().plot()

(df+1).cumprod()[-1:]

 

compare normalized stocks returns

 

The plot shows the growth of $1 invested on 1st Jan 2013 until 10th Oct 2020. For every $1 you invested in Apple in 2013 you would now have approximately $7 and so-forth. 

Symbols         AAPL       AMZN      MSFT     GOOGL      FB      Port
Date                                                                 
2020-10-01  6.831944  12.518985  9.141418  4.110369  9.5225  8.936468

 

 

 Sharpe Ratio

 

The Sharpe ratio is the most common ratio for comparing reward (return on investment) to risk (standard deviation). This allows us to adjust the returns on an investment by the amount of risk that was taken in order to achieve it. The Sharpe ratio also provides a useful metric to compare investments. The calculations are as follows:

\(\text{Sharpe ratio} = \frac{\bar{R}-R_f}{\sigma}\)

 

\(\bar{R} \):  annual expected return of the asset in question. 

\(R_f\) : annual risk-free rate. Think of this like a deposit in the bank earning x% per annum.  

\(\sigma\)  :  annualized standard deviation of returns

 

Since our data frequency is daily we need to annualize the expected return and standard deviation. This can be achieved by multiplying the daily average return by 255. And multiplying the daily standard deviation by \(\sqrt 255\). For simplicity we will assume that the risk-free rate \(R_f\) = 1% throughout the 7 year period. 

 

Python code to calculate Sharpe ratio:

def sharpe_ratio(return_series, N, rf):
    mean = return_series.mean() * N -rf
    sigma = return_series.std() * np.sqrt(N)
    return mean / sigma

N = 255 #255 trading days in a year
rf =0.01 #1% risk free rate
sharpes = df.apply(sharpe_ratio, args=(N,rf,),axis=0)

sharpes.plot.bar()

 

 

The interpretation of the Sharpe ratio is that higher numbers relate to better risk-adjusted investments. Recall the total return numbers we calculated previously, and notice that even though the majority of the individual stocks had a higher return, they also had a higher standard deviation.

 

Sortino Ratio

The Sortino ratio is very similar to the Sharpe ratio, the only difference being that where the Sharpe ratio uses all the observations for calculating the standard deviation the Sortino ratio only considers the harmful variance. So in the plot below, we are only considering the deviations colored red. The rationale for this is that we aren't too worried about positive deviations, however, the negative deviations are of great concern, since they represent loss of our money. 

 

 

 \(\text{Sortino ratio} = \frac{\bar{R}-R_f}{\sigma^-}\)

 

Everything in the ratio above is the same as the Sharpe ratio except \(\sigma^-\) represents the annualized down-side standard deviation. 

Python code to calculate and plot the results. 

def sortino_ratio(series, N,rf):
    mean = series.mean() * N -rf
    std_neg = series[series<0].std()*np.sqrt(N)
    return mean/std_neg


sortinos = df.apply(sortino_ratio, args=(N,rf,), axis=0 )
sortinos.plot.bar()
plt.ylabel('Sortino Ratio')

 

sortino ratio with python

 

 

It looks like Amazon and the portfolio are almost neck-and-neck in terms of performance for this ratio. As with the Sharpe ratio, higher values are preferable.

 

Max Drawdown

Max drawdown quantifies the steepest decline from peak to trough observed for an investment. This is useful for a number of reasons, mainly the fact that it doesn't rely on the underlying returns being normally distributed. It also gives us an indication of conditionality amongst the returns increments. Whereas in the previous ratios, we only considered the overall reward relative to risk, however, it may be that consecutive returns are not independent leading to unacceptably high losses of a given period of time. 

 

To calculate max drawdown first we need to calculate a series of drawdowns as follows:

\(\text{drawdowns} = \frac{\text{peak-trough}}{\text{peak}}\)

 

We then take the minimum of this value throughout the period of analysis. 

 

max drawdown visualized with python

 

Python code to calculate max drawdown for the stocks listed above. 

def max_drawdown(return_series):
    comp_ret = (return_series+1).cumprod()
    peak = comp_ret.expanding(min_periods=1).max()
    dd = (comp_ret/peak)-1
    return dd.min()


max_drawdowns = df.apply(max_drawdown,axis=0)
max_drawdowns.plot.bar()
plt.yabel('Max Drawdown')

 

max drawdowns comparison python

 

I believe max drawdown is usually presented as an absolute value, however, I will leave it the way it is for the time being. It looks like the portfolio performed the best according to max drawdown during the 7 year period we analyzed. The max drawdown should be interpreted as; numbers closer to zero are preferable. 

 

 

 

 Calmar Ratio

The final risk/reward ratio we will consider is the Calmar ratio. This is similar to the other ratios, with the key difference being that the Calmar ratio uses max drawdown in the denominator as opposed to standard deviation. 

 

\(\text{Calmar ratio} = \frac{\bar{R}}{\text{max drawdown}}\)

 

calmars = df.mean()*255/abs(max_drawdowns)

calmars.plot.bar()
plt.ylabel('Calmar ratio')

 

calmar ratios comparison python

 

 

It appears that Microsoft performs the best according to this ratio. As with the Sharpe and Sortino, higher values are preferable.

 

 

 

Wrapping up

 

If you would like to see these ratios applied to a more realistic backtest you can take a look at this crypto-algo trading example

 

Let's combine all the ratios we have calculated and put them in a pandas dataframe.

 

btstats = pd.DataFrame()
btstats['sortino'] = sortinos
btstats['sharpe'] = sharpes
btstats['maxdd'] = max_drawdowns
btstats['calmar'] = calmars

btstats
Out: 
         sortino    sharpe     maxdd    calmar
Symbols                                        
AAPL     1.286527  0.988260 -0.385159  0.758557
AMZN     1.678367  1.194467 -0.341038  1.107348
MSFT     1.549506  1.181460 -0.280393  1.158764
GOOGL    1.127802  0.808867 -0.308708  0.704477
FB       1.413423  0.994674 -0.429609  0.823078
Port     1.676891  1.311674 -0.274688  1.140057

 

 

Plotting dataframe as table

 

(df+1).cumprod().plot(figsize=(8,5))
plt.table(cellText=np.round(btstats.values,2), colLabels=btstats.columns,
          rowLabels=btstats.index,rowLoc='center',cellLoc='center',loc='top',
          colWidths=[0.25]*len(btstats.columns))
plt.tight_layout()

 

plot pandas dataframe as table