Finance notebooks

Some basics: Portfolio finance

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
Date
2016-12-16 115.97 809.84 62.30 41.74 123.24
2016-12-19 116.64 812.50 63.62 41.67 122.99
2016-12-20 116.95 815.20 63.54 41.66 123.33
2016-12-21 117.06 812.20 63.54 41.57 123.18
2016-12-22 116.29 809.68 63.55 41.55 123.72
2016-12-23 116.52 807.80 63.24 41.60 123.14
2016-12-27 117.26 809.93 63.28 41.61 123.07
2016-12-28 116.76 804.57 62.99 41.39 122.68
2016-12-29 116.73 802.88 62.90 41.60 122.79
2016-12-30 115.82 792.45 62.14 41.46 121.72

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
Compound Annual Return 0.267186 0.159332 0.119102 0.104706 0.142054

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
Annual Volatility 0.33434 0.303486 0.288101 0.191743 0.194047

Let's join these stats into a single dataframe.


In [5]:
stock_moments = pd.concat((compound_returns, annual_volatility))
stock_moments


Out[5]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
Compound Annual Return 0.267186 0.159332 0.119102 0.104706 0.142054
Annual Volatility 0.334340 0.303486 0.288101 0.191743 0.194047

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]:
Equally-weighted Portfolio
Compound Annual Return 0.158476
Annual Volatility NaN

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
NASDAQ:AAPL 0.111783 0.054017 0.044741 0.021701 0.024063
NASDAQ:GOOGL 0.054017 0.092104 0.046194 0.022491 0.024703
NASDAQ:MSFT 0.044741 0.046194 0.083002 0.026737 0.026076
NYSE:KO 0.021701 0.022491 0.026737 0.036766 0.018623
NYSE:MCD 0.024063 0.024703 0.026076 0.018623 0.037654

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
Annual Volatility 0.33434 0.303486 0.288101 0.191743 0.194047

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]:
Equally-weighted Portfolio
Compound Annual Return 0.158476
Annual Volatility 0.19799

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]:
NASDAQ:AAPL NASDAQ:GOOGL NASDAQ:MSFT NYSE:KO NYSE:MCD
0 0.270845 0.269795 0.033014 0.269393 0.156953
1 0.111105 0.125138 0.076527 0.334278 0.352952
2 0.221398 0.291408 0.212823 0.199991 0.074381
3 0.344858 0.134316 0.305975 0.187393 0.027459
4 0.357855 0.089550 0.050103 0.206363 0.296129
5 0.118283 0.202157 0.165559 0.332108 0.181892
6 0.133565 0.382373 0.025669 0.132206 0.326186
7 0.320789 0.293403 0.045960 0.106839 0.233009
8 0.092553 0.166488 0.251615 0.214966 0.274378
9 0.289671 0.194250 0.244769 0.093943 0.177367
10 0.001529 0.306609 0.237360 0.210079 0.244422
11 0.030979 0.248186 0.358043 0.335325 0.027467
12 0.233896 0.209883 0.187263 0.113956 0.255003
13 0.127375 0.349819 0.243117 0.209296 0.070393
14 0.244928 0.238751 0.096907 0.190146 0.229269
15 0.217726 0.228832 0.270688 0.260237 0.022516
16 0.210963 0.157986 0.133615 0.204858 0.292578
17 0.058084 0.010331 0.290621 0.292005 0.348959
18 0.002741 0.134046 0.292271 0.273089 0.297853
19 0.267309 0.104100 0.105956 0.305210 0.217425
20 0.003448 0.352006 0.147571 0.124752 0.372223
21 0.235984 0.058046 0.349729 0.008997 0.347243
22 0.240220 0.034372 0.217418 0.214501 0.293489
23 0.090188 0.215243 0.052392 0.396912 0.245265
24 0.041359 0.263153 0.330124 0.343638 0.021726
25 0.144372 0.155296 0.233688 0.274134 0.192510
26 0.345813 0.176227 0.358450 0.103551 0.015959
27 0.052147 0.305163 0.033918 0.312451 0.296321
28 0.316725 0.244629 0.120642 0.244598 0.073407
29 0.328365 0.047474 0.046881 0.296114 0.281165
... ... ... ... ... ...
4970 0.214880 0.211289 0.225575 0.139862 0.208394
4971 0.337624 0.194843 0.048704 0.077848 0.340980
4972 0.117415 0.134156 0.360775 0.370548 0.017105
4973 0.060317 0.351885 0.196298 0.136051 0.255450
4974 0.120642 0.203386 0.151436 0.296496 0.228040
4975 0.148360 0.022826 0.309564 0.304206 0.215044
4976 0.135754 0.155022 0.434334 0.007749 0.267141
4977 0.244124 0.014366 0.204634 0.320808 0.216068
4978 0.111145 0.101286 0.102537 0.560826 0.124206
4979 0.213932 0.144489 0.249168 0.210600 0.181812
4980 0.120485 0.054302 0.106353 0.182800 0.536060
4981 0.265560 0.241151 0.181447 0.186697 0.125145
4982 0.169202 0.406287 0.075063 0.250418 0.099030
4983 0.214124 0.242224 0.102193 0.348768 0.092690
4984 0.010993 0.305174 0.266043 0.222501 0.195290
4985 0.326933 0.000790 0.161160 0.285724 0.225393
4986 0.225653 0.116264 0.189220 0.413489 0.055375
4987 0.297431 0.083251 0.110296 0.248057 0.260965
4988 0.094514 0.278716 0.227468 0.046813 0.352490
4989 0.214279 0.357998 0.015431 0.084698 0.327593
4990 0.294601 0.293040 0.225896 0.168603 0.017860
4991 0.261091 0.261858 0.205711 0.006787 0.264553
4992 0.275680 0.221310 0.166466 0.203347 0.133196
4993 0.053932 0.100323 0.187076 0.327107 0.331562
4994 0.119015 0.227067 0.287330 0.152059 0.214528
4995 0.414266 0.215501 0.271605 0.024639 0.073989
4996 0.219560 0.234166 0.123408 0.287956 0.134910
4997 0.205058 0.033264 0.379035 0.336031 0.046612
4998 0.112209 0.382946 0.186375 0.278648 0.039822
4999 0.362273 0.026392 0.215035 0.160426 0.235874

5000 rows × 5 columns

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]:
Compound Annual Return Annual Volatility
0 0.169788 0.202491
1 0.143878 0.173173
2 0.162439 0.213816
3 0.173506 0.224855
4 0.179523 0.199148
5 0.144145 0.186198
6 0.159847 0.201044
7 0.182219 0.215465
8 0.142708 0.188574
9 0.172531 0.215509
10 0.134249 0.195578
11 0.129477 0.206070
12 0.166394 0.203284
13 0.150640 0.213285
14 0.167502 0.199652
15 0.157320 0.212187
16 0.160464 0.189515
17 0.131925 0.180604
18 0.127806 0.184630
19 0.163471 0.189657
20 0.140521 0.195177
21 0.164223 0.208912
22 0.159706 0.190525
23 0.141032 0.177484
24 0.131365 0.205248
25 0.147201 0.190318
26 0.176277 0.236501
27 0.141404 0.182901
28 0.174009 0.213590
29 0.171828 0.190779
... ... ...
4970 0.162192 0.204169
4971 0.183643 0.207834
4972 0.136945 0.202649
4973 0.146095 0.202265
4974 0.146115 0.185102
4975 0.142546 0.189017
4976 0.151462 0.215848
4977 0.156172 0.188456
4978 0.134413 0.177253
4979 0.157736 0.198976
4980 0.148801 0.175729
4981 0.168313 0.210355
4982 0.159171 0.210840
4983 0.157662 0.196849
4984 0.134286 0.198870
4985 0.168607 0.195314
4986 0.152513 0.194196
4987 0.168915 0.192712
4988 0.151727 0.201086
4989 0.171535 0.207476
4990 0.172500 0.227047
4991 0.174274 0.217111
4992 0.168959 0.207972
4993 0.134026 0.175340
4994 0.148596 0.201284
4995 0.190461 0.243622
4996 0.159987 0.197648
4997 0.147039 0.205336
4998 0.148027 0.211003
4999 0.176915 0.207784

5000 rows × 2 columns

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 [ ]: