Let's make some portfolio math, as usual with python (https://www.python.org/) and pandas (http://pandas.pydata.org/).
In [1]:
import pandas as pd
import pandas_datareader.data as web
import matplotlib.pyplot as plt
import numpy as np
# Defines the chart color scheme using Tableu's Tableau10
plt.style.use('https://gist.githubusercontent.com/mbonix/8478091db6a2e6836341c2bb3f55b9fc/raw/7155235ed03e235c38b66c160d402192ad4d94d9/tableau10.mplstyle')
%matplotlib inline
We are going to download some prices, just as an example. We'll work on Apple (AAPL), Alphabet (former Google, GOOGL), Microsoft (MSFT), McDonald's (MCD), Coca-Cola (KO) over a ten-year period.
In [2]:
# List of stocks tickers
STOCKS = ['NASDAQ:AAPL', 'NASDAQ:GOOGL', 'NASDAQ:MSFT', 'NYSE:MCD', 'NYSE:KO']
# Analysis period
START = '12-30-2006'
END = '12-31-2016'
data = web.DataReader(STOCKS, 'google', pd.to_datetime(START), pd.to_datetime(END))
# Only Close prices are relevant for analysis
prices = data.loc['Close', :, :]
prices.tail(10)
Out[2]:
Now, let's calculate an annualized return for each of the stocks over the entire period.
In [3]:
compound_returns = (1 + (prices.iloc[-1, :]).to_frame().T / (prices.iloc[0, :])) ** 0.1 - 1
compound_returns.index = ['Compound Annual Return']
compound_returns
Out[3]:
Then we could compute an annual volatility over the period, daily sampled.
In [4]:
annual_volatility = prices.fillna(method='pad').pct_change().std().apply(lambda x: x * 260 ** 0.5).to_frame().T
annual_volatility.index = ['Annual Volatility']
annual_volatility
Out[4]:
Let's join these stats into a single dataframe.
In [5]:
stock_moments = pd.concat((compound_returns, annual_volatility))
stock_moments
Out[5]:
And let's make a chart, plotting volatities on X-axis and returns on Y-axis.
In [6]:
f = plt.figure(figsize=(10, 8));
ax = f.add_subplot(111);
for n in range(len(stock_moments.columns)):
ax.plot(stock_moments.loc['Annual Volatility', stock_moments.columns[n]], stock_moments.loc['Compound Annual Return', stock_moments.columns[n]], ls='', marker='o', label=stock_moments.columns[n]);
ax.legend(loc='best');
ax.set_xlabel(stock_moments.index[1]);
ax.set_ylabel(stock_moments.index[0]);
ax.set_title('Stock risk reward profile');
A few considerations: Apple (AAPL) has delivered a superb return over the period, but at the cost of the highest volatility (risk), while the old-economy stocks (Coca-Cola: KO e McDonald's: MCD) have been characterized by a lower risk and a lower return.
Now, let's calculate the risk-reward profile of a equally-weighted portfolio composed by these five stocks. First of all, the return of the portfolio, which is the average of the stock returns, equally-weighted.
In [7]:
WEIGHTS = [0.2, 0.2, 0.2, 0.2, 0.2]
port_moments = pd.DataFrame(index=stock_moments.index, columns=['Equally-weighted Portfolio'])
port_moments.loc['Compound Annual Return', 'Equally-weighted Portfolio'] = (stock_moments.loc['Compound Annual Return', :] * WEIGHTS).sum()
port_moments
Out[7]:
In order to calculate volatility, we need a covariance matrix. Luckily enough, Pandas has a function to do exactly that!
In [8]:
cov_mat = prices.fillna(method='pad').pct_change().cov().apply(lambda x: x * 260)
cov_mat
Out[8]:
Let's check that the roots of the diagonal of the matrix are the (annualized) standard deviations of the stocks.
In [9]:
pd.DataFrame(data=[cov_mat.iloc[n, n] ** 0.5 for n in range(len(cov_mat.columns))], index=cov_mat.columns, columns=['Annual Volatility']).T
Out[9]:
The volatility of the portfolio is easily computed.
In [10]:
port_moments.loc['Annual Volatility', 'Equally-weighted Portfolio'] = np.array(WEIGHTS).dot(cov_mat.values).dot(np.array(WEIGHTS).T) ** 0.5
port_moments
Out[10]:
So, we can plot the risk-reward profile of the portfolio on the previous chart.
In [11]:
f = plt.figure(figsize=(10, 8));
ax = f.add_subplot(111);
for n in range(len(stock_moments.columns)):
ax.plot(stock_moments.loc['Annual Volatility', stock_moments.columns[n]], stock_moments.loc['Compound Annual Return', stock_moments.columns[n]], ls='', marker='o', label=stock_moments.columns[n]);
ax.plot(port_moments.loc['Annual Volatility','Equally-weighted Portfolio'], port_moments.loc['Compound Annual Return', 'Equally-weighted Portfolio'], ls='', marker='d', label='Equally-weighted Portfolio');
ax.legend(loc='best');
ax.set_xlabel(stock_moments.index[1]);
ax.set_ylabel(stock_moments.index[0]);
ax.set_title('Stock risk reward profile');
We can notice that the portfolio risk profile is similar to old economy stocks, but the historical return benefits from the higher returns of tech stocks. Diversification at work!
Now, let's build some random portfolios and calculate their risk-reward profile. First of all, the weights matrix.
In [77]:
wgts = pd.DataFrame(np.array(np.random.uniform(0, 1, (5000, 5))), columns=prices.columns)
w = wgts.apply(lambda x: x / wgts.sum(axis=1))
w
Out[77]:
Let's compute first two moments of the simulated portfolios.
In [78]:
sim_ports = pd.DataFrame(index=w.index, columns=port_moments.index)
sim_ports.loc[:, 'Compound Annual Return'] = (stock_moments.loc['Compound Annual Return', :] * w).sum(axis=1)
sim_ports.loc[:, 'Annual Volatility'] = np.diag(w.values.dot(cov_mat.values).dot(w.values.T)) ** 0.5
sim_ports
Out[78]:
And let's plot those fancy random portfolios on the risk-reward chart.
In [79]:
f = plt.figure(figsize=(10, 8));
ax = f.add_subplot(111);
ax.plot(sim_ports.loc[0, 'Annual Volatility'], sim_ports.loc[0, 'Compound Annual Return'], ls='', marker='.', color='#cccccc', label='Random-weighted Portfolios');
for m in range(len(sim_ports.index)):
ax.plot(sim_ports.loc[m, 'Annual Volatility'], sim_ports.loc[m, 'Compound Annual Return'], ls='', marker='.', color='#cccccc');
for n in range(len(stock_moments.columns)):
ax.plot(stock_moments.loc['Annual Volatility', stock_moments.columns[n]], stock_moments.loc['Compound Annual Return', stock_moments.columns[n]], ls='', marker='o', label=stock_moments.columns[n]);
ax.plot(port_moments.loc['Annual Volatility','Equally-weighted Portfolio'], port_moments.loc['Compound Annual Return', 'Equally-weighted Portfolio'], ls='', marker='d', label='Equally-weighted Portfolio');
ax.legend(loc='best');
ax.set_xlabel(stock_moments.index[1]);
ax.set_ylabel(stock_moments.index[0]);
ax.set_title('Stock risk reward profile');
We can see that some of the random-weighted portfolios have a better risk-reward profile than the equally-weighted portfolio one and also than each of the stocks profile. So, there is plenty of space for portfolio optimization, but... this is another topic.
In [ ]: