Finding the Max Sharpe Ratio Portfolio

We've already seen that given a set of expected returns and a covariance matrix, we can plot the efficient frontier. In this section, we'll extend the code to locate the point on the efficient frontier that we are most interested in, which is the tangency portfolio or the Max Sharpe Ratio portfolio. Let's start by the usual imports, and load in the data.

In [125]:
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import PortfolioAnalytics as PortfolioAnalytics
import PortfolioLoadData as PortfolioLoadData
import PortfolioRisk as PortfolioRisk
import PortfolioStatistics as PortfolioStatistics
import PortfolioBacktest as PortfolioBacktest

%load_ext autoreload
%autoreload 2

ind = PortfolioLoadData.get_ind_returns()
er = PortfolioStatistics.annualize_rets(ind["1996":"2000"], 12)
cov = ind["1996":"2000"].cov()
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
In [126]:
PortfolioAnalytics.plot_ef(20, er, cov, style='-', show_cml=True, riskfree_rate=0.1)

Lack of Robustness of the Markowitz procedure and the GMV portfolio

Although the promise of the Markowitz procedure is exciting, in tends to fall apart in practice. The problem is that we rarely know Expected Returns and Expected Covariance in advance. Our estimates almost certainly contain some estimation error, and we'll see that the procedure is highly sensitive to these errors, which tend to get exaggerated in the portfolio.

One way to avoid this estimation game is to skip the entire process and just rely on naive diversification, which means hold all stocks with equal weight.

In [127]:
PortfolioAnalytics.plot_ef(20, er, cov, show_cml=True, riskfree_rate=0.1, show_ew=True)

Researchers have shown that the EW portfolio is a remarkably good portfolio to hold. In fact, there is overwhelming siupport for the idea that it is a far better portfolio to hold than a cap-weighted equivalent. As you can see, the EW portfolio is far inside the efficient frontier, but it requires no estimation whatsoever.

However, there is another point on the efficient frontier that is very interesting. This is the nose of the hull, which is the portfolio of lowest volatility across all possible portfolios. This is called the Minimum Volatility or the Global Minimum Volatility or GMV portfolio.

The interesting thing about it is that if you assume that all returns are the same, the optimizer cannot improve the sharpe ratio through raising returns, and so it must do so my lowering volatility. This means that if we just skip any returns estimation and assume all returns have the return, we'd get the weights of the GMV portfolio!

In [128]:
PortfolioAnalytics.plot_ef(20, er, cov, show_cml=True, riskfree_rate=0.1, show_ew=True, show_gmv=True)
Out[128]:
<matplotlib.axes._subplots.AxesSubplot at 0x27ff5bd24a8>

Implementing Portfolio Insurance (CPPI) and Drawdown Constraints

We'll start by implementing the basic Constant Proportion Portfolio Insurance dynamic risk budgeting algorithm, and test it against different portfolios. The CPPI concept is also known as the Constant Risk of Ruin Principles

In [129]:
# Load the industry returns and the total market index
ind_return = PortfolioLoadData.get_ind_returns()
tmi_return = PortfolioLoadData.get_total_market_index_returns()

ind_return.head()
Out[129]:
Food Beer Smoke Games Books Hshld Clths Hlth Chems Txtls ... Telcm Servs BusEq Paper Trans Whlsl Rtail Meals Fin Other
1926-07 0.0056 -0.0519 0.0129 0.0293 0.1097 -0.0048 0.0808 0.0177 0.0814 0.0039 ... 0.0083 0.0922 0.0206 0.0770 0.0193 -0.2379 0.0007 0.0187 0.0037 0.0520
1926-08 0.0259 0.2703 0.0650 0.0055 0.1001 -0.0358 -0.0251 0.0425 0.0550 0.0814 ... 0.0217 0.0202 0.0439 -0.0238 0.0488 0.0539 -0.0075 -0.0013 0.0446 0.0676
1926-09 0.0116 0.0402 0.0126 0.0658 -0.0099 0.0073 -0.0051 0.0069 0.0533 0.0231 ... 0.0241 0.0225 0.0019 -0.0554 0.0005 -0.0787 0.0025 -0.0056 -0.0123 -0.0386
1926-10 -0.0306 -0.0331 0.0106 -0.0476 0.0947 -0.0468 0.0012 -0.0057 -0.0476 0.0100 ... -0.0011 -0.0200 -0.0109 -0.0508 -0.0264 -0.1538 -0.0220 -0.0411 -0.0516 -0.0849
1926-11 0.0635 0.0729 0.0455 0.0166 -0.0580 -0.0054 0.0187 0.0542 0.0520 0.0311 ... 0.0163 0.0377 0.0364 0.0384 0.0160 0.0467 0.0652 0.0433 0.0224 0.0400

5 rows × 30 columns

The CPPI algorithm is surprisingly simple to implement. This takes as input, the returns of a risky asset and a safe asset, along with the initial wealth to invest at the start, along with a floor that should not be violated.

In [130]:
btr = PortfolioBacktest.run_cppi(ind_return["2000":][["Steel", "Fin", "Beer"]])
ax = btr["Wealth"].plot(figsize=(12,5))
btr["Risky Wealth"].plot(ax=ax, style="--")
Out[130]:
<matplotlib.axes._subplots.AxesSubplot at 0x27ff6c9a320>
In [131]:
# with CPPI
PortfolioBacktest.summary_stats(btr["Wealth"].pct_change().dropna())
Out[131]:
Annualized Return Annualized Vol Skewness Kurtosis Cornish-Fisher VaR (5%) Historic CVaR (5%) Sharpe Ratio Max Drawdown
Steel -0.005167 0.174180 -1.995143 17.110190 0.091995 0.130153 -0.196750 -0.655198
Fin 0.040894 0.131678 -0.946504 6.051414 0.065535 0.091621 0.080352 -0.549673
Beer 0.075544 0.115462 -0.669250 4.760879 0.052923 0.074908 0.383772 -0.259582
In [132]:
# without CPPI
PortfolioBacktest.summary_stats(btr["Risky Wealth"].pct_change().dropna())
Out[132]:
Annualized Return Annualized Vol Skewness Kurtosis Cornish-Fisher VaR (5%) Historic CVaR (5%) Sharpe Ratio Max Drawdown
Steel -0.001320 0.312973 -0.330333 4.132666 0.150415 0.208117 -0.097801 -0.758017
Fin 0.057941 0.192986 -0.543630 5.015294 0.091172 0.132175 0.140661 -0.718465
Beer 0.082286 0.139058 -0.502368 4.181533 0.063037 0.091442 0.365788 -0.271368

Insurance strategies usually help with drawdowns, but they can also be adapted to explictly limit the drawdown.You can now call run_cppi witk a parameter drawdown. For instance, to run CPPI and limit the drawdown to 20%

In [133]:
btr = PortfolioBacktest.run_cppi(ind_return["2000":][["Steel", "Fin", "Beer"]], drawdown=0.20)
ax = btr["Wealth"].plot(figsize=(12,5))
btr["Risky Wealth"].plot(ax=ax, style="--")
Out[133]:
<matplotlib.axes._subplots.AxesSubplot at 0x27ff6e9d0f0>
In [134]:
# with drawdown limit of 20%
PortfolioBacktest.summary_stats(btr["Wealth"].pct_change().dropna())[["Annualized Return", "Annualized Vol", "Sharpe Ratio", "Max Drawdown"]]
Out[134]:
Annualized Return Annualized Vol Sharpe Ratio Max Drawdown
Steel 0.038389 0.097127 0.083941 -0.197943
Fin 0.052602 0.073573 0.298912 -0.193668
Beer 0.056568 0.072510 0.356528 -0.125096
In [135]:
# no limit
PortfolioBacktest.summary_stats(btr["Risky Wealth"].pct_change().dropna())[["Annualized Return", "Annualized Vol", "Sharpe Ratio", "Max Drawdown"]]
Out[135]:
Annualized Return Annualized Vol Sharpe Ratio Max Drawdown
Steel -0.001320 0.312973 -0.097801 -0.758017
Fin 0.057941 0.192986 0.140661 -0.718465
Beer 0.082286 0.139058 0.365788 -0.271368
In [136]:
# and for the total market

btr = PortfolioBacktest.run_cppi(tmi_return["1999":], drawdown=0.20)
ax = btr["Wealth"].plot(figsize=(12,5))
btr["Risky Wealth"].plot(ax=ax, style="--")
Out[136]:
<matplotlib.axes._subplots.AxesSubplot at 0x27ff6de9a58>
In [137]:
# with CPPI Insurance
PortfolioBacktest.summary_stats(btr["Wealth"].pct_change().dropna())[["Annualized Return", "Annualized Vol", "Sharpe Ratio", "Max Drawdown"]]
Out[137]:
Annualized Return Annualized Vol Sharpe Ratio Max Drawdown
R 0.049785 0.06419 0.299906 -0.174311
In [138]:
# without CPPI Insurance
PortfolioBacktest.summary_stats(btr["Risky Wealth"].pct_change().dropna())[["Annualized Return", "Annualized Vol", "Sharpe Ratio", "Max Drawdown"]]
Out[138]:
Annualized Return Annualized Vol Sharpe Ratio Max Drawdown
R 0.06137 0.148571 0.205314 -0.499943
In [ ]: