Lesson 4: Implement Markowitz Portfolio Optimization in Only 3 Lines of Code
Use fastquant to maximize the returns of your stock portfolio given its overall risk profile
- Some Basic Ideas
- Modern Portfolio Theory (Markowitz Model)
- Finding and plotting the Optimal Weights (the hard way)
- Finding and plotting the Optimal Weights (the fastquant way)
- Bonus Section : Interactive Charts
# uncomment to install in colab
# !pip3 install fastquant
Some Basic Ideas
Stock or Share is a unit of ownership in a company. When you invest in the stock market, (stock market is basically a place for buying or selling stocks) there are 2 main ways of earning:
- Dividend - This is an amount paid to you by a company for your investment.
- Stock Trading - The profit that you make for buying/selling stocks.
- Portfolio - A combination of assets of an individual / investor.
Fundamentally, you can earn money by buying some stocks, in the hope that it's price will increase in the future.
There are actually clever ways on how to earn even if you predict that a stock price will decline, but that's outside the scope of this lecture.
So in this lecture, we'll oversimplify and what we want is to buy a stock cheap, and sell it when its price has increased because that way we will make a profit. Otherwise we will incur a loss, if we decided to sell a stock at a cheaper price.
Modern Portfolio Theory (Markowitz Model)
As mentioned above, investing in the stock market can result in either profit or loss.
In a nutshell, Modern Portfolio Theory is a way of maximizing return for a given risk. We will define what return and risk means shortly.
Let's understand this by using an example.
Suppose you wanted to invest in the stock market. After completing your research, you decided to invest in the following companies:
- MEG
- MAXS
- JFC
- ALI
We will download the data for this using a python library called fastquant. It was actually developed by a fellow Filipino Data Scientist. It aims to democratize data-driven investments for everyone.
NOTE The model we'll be using relies on the assumption that returns are normally distributed. Therefore, it helps if we have large number of data points.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
import scipy.optimize as optimization
from fastquant import get_stock_data
warnings.filterwarnings('ignore')
%matplotlib inline
stocks = ['MEG', 'MAXS', 'JFC', 'ALI']
datas = []
for i in stocks:
df = get_stock_data(i, "2017-01-01", "2020-01-01")
df = df.reset_index()
df.columns = ['DATE',i]
df = df[['DATE',i]]
datas.append(df)
datas1 = pd.merge(datas[0],datas[1],on=['DATE'])
datas2 = pd.merge(datas[2],datas[3],on=['DATE'])
data = pd.merge(datas1,datas2,on=['DATE'])
data.index = data['DATE']
data.drop('DATE',axis=1,inplace=True)
The table below shows the first 5 entries in our dataset. The values here are closing prices. A closing price is a price of a stock at the end of a given trading day.
data.head()
Now, let's ask ourselves. Why don't we invest in a single company, instead of investing in multiple companies?
Modern Portfolio Theory tells us that we can minimize our loss thru diversification. Let's understand this with an example.
Suppose you decided to invest on January 2017. For illustraton purposes, let's consider the period January 2017 - May 2018.
- Case 1: You invested solely on MAXS
- Case 2: You decided to invest 50% to MAXS and the other 50% to JFC
If you decided to go with case 1, it would be clear that you could immediately lose some money (as the chart shows a decreasing trend). If you instead decided to go with Case 2, your loss could have been mitigated since the price for JFC is increasing during that period.
Of course you could argue that "why not invest all of my money in JFC", well my counter argument to that would be, when JFC is experiencing a decline in it's price, there would be some other company that's actually experiencing an increase in it's price.
Key Takeaway
- Invest in multiple stocks as much as possible, to minimize your loss. (Technically uncorrelated or negatively correlated)
data['MAXS'].plot(figsize=(12,5),legend=True)
data['JFC'].plot(figsize=(12,5),legend=True,color='r')
Now, let's define what a return is. Intuitively, we can define return as :
The stock price today minus the stock price yesterday. Divide the difference by the stock price yesterday
More formally,
The return $R_{t,t+1}$ from time $t$ to time ${t+1}$ is given by:
$$ R_{t,t+1} = \frac{P_{t+1}-P_{t}}{P_{t}} $$
where $P_i$ is the price of the stock for a given time point.
returns = data.pct_change()
returns
The mean of the returns is called the Expected Return.
Similarly, the Risk or Volatility is the standard deviation of the returns.
(This is different from the expected return and volatility of a portfolio, this is for a single stock)
returns.mean()
returns.std()
We'll only plot MAXS and MEG to emphasize that the return for MEG is more volatile.
returns[['MAXS','MEG']].plot(figsize=(12,5))
Expected Return and Risk of a Portfolio
Suppose your portfolio consists of returns $R_1, R_2, R_3, ... ,R_n$. Then, the expected return of a portfolio is given by:
$E(R) = w_1E(R_1) + w_2E(R_2) + w_3E(R_3) + ... + w_nE(R_n) $
where $w_i$ is the $i$th component of an $n-dimensional$ vector, and $\Sigma w_i = 1.$
weights = np.random.random(len(stocks))
weights /= np.sum(weights)
weights
returns.mean()
def calculate_portfolio_return(returns, weights):
portfolio_return = np.sum(returns.mean()*weights)*252
print("Expected Portfolio Return:", portfolio_return)
calculate_portfolio_return(returns,weights)
If you had a course in Probability, you might recall that expectation of a random variable is linear while the variance is not. That's the same argument why the formula for the variance of a portfolio is quite more complicated.
$Var(R) = \bf{w^{T}}\Sigma \textbf{w}$
where $\Sigma$ is the covariance matrix of $R_i$
returns.cov()
np.sqrt(returns.cov())
returns.std()
def calculate_portfolio_risk(returns, weights):
portfolio_variance = np.sqrt(np.dot(weights.T, np.dot(returns.cov()*252,weights)))
print("Expected Risk:", portfolio_variance)
calculate_portfolio_risk(returns,weights)
Sharpe Ratio
Remember, what we want is to find the best possible weight vector $\bf{w}$ that would give us the best possible return, with a minimal risk. Therefore, we will introduce a new metric called the sharpe ratio. It's simply equal to
$$S.R. = \frac{E(R) - R_f}{\sqrt{Var(R)}}$$
where $R_f$ is the risk free return. Since we're only limiting ourselves to risky assets (stocks) therefore, the formula becomes
$$S.R. = \frac{E(R) - 0}{\sqrt{Var(R)}} = \frac{E(R)}{\sqrt{Var(R)}}$$
def generate_portfolios(weights, returns):
preturns = []
pvariances = []
for i in range(10000):
weights = np.random.random(len(stocks))
weights/=np.sum(weights)
preturns.append(np.sum(returns.mean()*weights)*252)
pvariances.append(np.sqrt(np.dot(weights.T,np.dot(returns.cov()*252,weights))))
preturns = np.array(preturns)
pvariances = np.array(pvariances)
return preturns,pvariances
def plot_portfolios(returns, variances):
plt.figure(figsize=(10,6))
plt.scatter(variances,returns,c=returns/variances,marker='o')
plt.grid(True)
plt.xlabel('Expected Volatility')
plt.ylabel('Expected Return')
plt.colorbar(label='Sharpe Ratio')
plt.show()
preturns, pvariances = generate_portfolios(weights,returns)
plot_portfolios(preturns, pvariances)
def statistics(weights, returns):
portfolio_return=np.sum(returns.mean()*weights)*252
portfolio_volatility=np.sqrt(np.dot(weights.T,np.dot(returns.cov()*252,weights)))
return np.array([portfolio_return,portfolio_volatility,portfolio_return/portfolio_volatility])
def min_func_sharpe(weights,returns):
return -statistics(weights,returns)[2]
def optimize_portfolio(weights,returns):
constraints = ({'type':'eq','fun': lambda x: np.sum(x)-1})
bounds = tuple((0,1) for x in range(len(stocks)))
optimum=optimization.minimize(fun=min_func_sharpe,x0=weights,args=returns,method='SLSQP',bounds=bounds,constraints=constraints)
return optimum
def print_optimal_portfolio(optimum, returns):
print("Optimal weights:", optimum['x'].round(3))
print("Expected return, volatility and Sharpe ratio:", statistics(optimum['x'].round(3),returns))
def show_optimal_portfolio(optimum, returns, preturns, pvariances):
plt.figure(figsize=(10,6))
plt.scatter(pvariances,preturns,c=preturns/pvariances,marker='o')
plt.grid(True)
plt.xlabel('Expected Volatility')
plt.ylabel('Expected Return')
plt.colorbar(label='Sharpe Ratio')
plt.plot(statistics(optimum['x'],returns)[1],statistics(optimum['x'],returns)[0],'g*',markersize=20.0)
plt.show()
optimum=optimize_portfolio(weights,returns)
print_optimal_portfolio(optimum, returns)
show_optimal_portfolio(optimum, returns, preturns, pvariances)
from fastquant import Portfolio
stock_list = ['MEG', 'MAXS', 'JFC', 'ALI']
p = Portfolio(stock_list,"2017-01-01", "2020-01-01")
axs = p.data.plot(subplots=True, figsize=(15,10))
fig = p.plot_portfolio(N=1000)
Bonus Section : Interactive Charts
Refer to this link https://altair-viz.github.io/getting_started/installation.html for altair's installation, but most likely
!pip3 install altair
should do the trick.
import altair as alt
alt.renderers.set_embed_options(actions=False)
weights = pd.DataFrame(optimum['x'].round(2),columns=['weights'])
weights['Symbols'] = stocks
returns,variances = p.generate_portfolios(N=5000)
portfolios = pd.DataFrame()
portfolios['Expected Return'] = returns
portfolios['Expected Volatility'] = variances
portfolios['Sharpe Ratio'] = portfolios['Expected Return'] / portfolios['Expected Volatility']
minimumX = portfolios['Expected Volatility'].min()
maximumX = portfolios['Expected Volatility'].max()
minimumY = portfolios['Expected Return'].min()
maximumY = portfolios['Expected Return'].max()
optimum = p.calculate_statistics(p.optimum_weights)
optimal = pd.DataFrame()
optimal['Expected Return'] = [optimum[0]]
optimal['Expected Volatility'] = [optimum[1]]
optimal['Sharpe Ratio'] = [optimum[2]]
optimal['img'] = "https://img.icons8.com/clouds/100/000000/us-dollar--v1.png"
# uncomment if N > 5000
# alt.data_transformers.enable('json')
chart = alt.Chart(portfolios).mark_circle().encode(
x = alt.X('Expected Volatility',scale=alt.Scale(domain=[minimumX,maximumX])),
y = alt.X('Expected Return',scale=alt.Scale(domain=[minimumY,maximumY])),
color = alt.Color('Sharpe Ratio',scale=alt.Scale(range=['blue','yellow'])),
tooltip = ['Sharpe Ratio']
).properties(height=350,width=500)
optimal_chart = alt.Chart(optimal).mark_image(height=60,width=60).encode(
x = alt.X('Expected Volatility',scale=alt.Scale(domain=[minimumX,maximumX])),
y = alt.X('Expected Return',scale=alt.Scale(domain=[minimumY,maximumY])),
color = alt.Color('Sharpe Ratio',scale=alt.Scale(range=['blue','yellow'])),
tooltip = ['Sharpe Ratio'],
url = 'img'
).properties(height=350,width=500)
visualization = chart+optimal_chart
visualization
Resources:
- Financial Mathematics
- General Investing