18. Modern Portfolio Theory and Capital Asset Pricing Model#
Show code cell source
"""
Modern Portfolio Theory
Example with a 3-asset portfolio
"""
import numpy as np
from scipy.optimize import minimize
from functools import partial
import matplotlib.pyplot as plt
Assets
Expected value \(\boldsymbol{\mu}\) and covariance \(\boldsymbol{\sigma}^2\) of expected returns of single assets are defined.
Show code cell source
#> Dictionary collecting different assets for building portfolio
ptfs = {}
#> 1. Two-asset portfolio
# Here, un-correlated assets, r12 = 0
#> Expected returns and covariance
mu1, mu2 = 5., 10.
si1, si2 = 6., 15.
r12 = .0
mu = np.array([mu1, mu2])
si = np.array([
[ si1**2, si1*si2*r12],
[ si1*si2*r12, si2**2]
])
ptfs['risky'] = { 'mu': mu, 'si': si, 'marker': 'x' }
#> 2. Three-asset portfolio
# The same as the two-asset portfolio above, with an extra risk-free asset
# (risk-free = zero variance, si3 = .0)
#> Expected returns and covariance
mu1, mu2, mu3 = 5., 10., 3.
si1, si2, si3 = 6., 15., 0.
r12, r13, r23 = .0, .0, .0
mu = np.array([mu1, mu2, mu3])
si = np.array([
[ si1**2, si1*si2*r12, si1*si3*r12],
[ si1*si2*r12, si2**2, si2*si3*r13],
[ si1*si3*r13, si2*si3*r23, si3**2]
])
ptfs['with risk-free'] = { 'mu': mu, 'si': si, 'marker': 's' }
18.1. Modern Portfolio Theory#
Given a set of \(N\) assets with expected return \(\mu_i = \left\{ \boldsymbol\mu \right\}_i\), \(i = 1:N\), and covariance (matrix) \(\boldsymbol\sigma^2\), a portfolio is defined by the weights (percentage) \(w_i = \left\{ \mathbf{w} \right\}_i\) of the individual assets in the portfolio.
Return of the portfolio. The return of an amount \(x_0\) invested in a portfolio with asset allocation \(w_i\) is found evaluating the amount \(x_1\) after the period for which the return is defined,
Not-fully invested portfolio can be represented with an asset \(0\) (cash with zero return). The return is defined as
If (or as? Even with not-fully invested portfolio, or portfolio w/leverage) \(\sum_{i=1}^{N} w_i = 1\), the return of the portfolio reads
Expected value and variance of the return of the portfolio. The expected value and the variance of the return thus read
and
Optimal portfolio. The evaluation of weights \(\mathbf{w}^*\) of optimal asset allocation for MPT is recast as the optimization problem of finding asset allocation with minimum volatility (variance) for the given expected return \(\overline{\mu}\), under some constraints about asset allocation
where other constraints could be:
fully invested portfolio
\[\sum_{k} w_k = 1 \ ,\]no leverage on asset \(k\)
\[w_k \le 1 \ ,\]no short-selling on asset \(k\)
\[w_k \ge 0 \ .\]
Useful arrays and functions. Functions to be used in the optimization are defined here. The optimization process aims at finding the asset allocation with minimum variance of the return, given the expected value of the return.
Show code cell source
# Find min, max returns for plots
mu_v = np.concatenate( [ i['mu'] for k,i in ptfs.items() ] )
min_mu = np.min(mu_v)
max_mu = np.max(mu_v)
#> MPT
# An optimization problem is solved for a set of desired return to find the efficient
# frontier of a portfolio, under the assumptions of MPT
#> Array of desired returns, between min and max return of single assets
des_ret_v = np.linspace(min_mu, max_mu, 30)
#> Constraints
def eq_desired_return(x, mu, desired_ret):
return np.sum( x * mu ) - desired_ret
def eq_weight_sum(x):
return np.sum(x) - 1
#> Objective function
def ptf_var(x, sigma):
return x.T @ sigma @ x
18.2. Efficient frontier and CAPM with possibility of leverage and short-selling#
Show code cell source
for kptf, ptf in ptfs.items():
n_assets = len(ptf['mu'])
ptf['wmat'] = []
ptf['min_sig'] = []
ptf['des_ret'] = np.linspace(np.min(ptf['mu']), np.max(ptf['mu']), 30)
for desired_ret in ptf['des_ret']:
#> Constraints (just comment if you don't want some)
eq_cons = [
{'type': 'eq', 'fun': partial(eq_desired_return, mu=ptf['mu'], desired_ret=desired_ret)},
{'type': 'eq', 'fun': eq_weight_sum}, # sum(w) = 1 (fully invested)
]
# eq_cons += [ {'type': 'ineq', 'fun': lambda x: x[i]} for i in np.arange(n_assets) ]
# eq_cons += [ {'type': 'ineq', 'fun': lambda x: 1-x[i]} for i in np.arange(n_assets) ]
cost_fun = partial(ptf_var, sigma=ptf['si'])
x0 = np.zeros(n_assets); x0[0] = 1 # np.ones(3) / 3.
res = minimize(cost_fun, x0, constraints=eq_cons,)
# print(f"Desired Return: {desired_ret}, res: {res.x}")
ptf_si = np.sqrt(res.x @ ptf['si'] @ res.x)
ptf_mu = np.sum(res.x * ptf['mu'])
#> Store weights and min variance, for the desired expected return
ptf['wmat'] += [ res.x ]
ptf['min_sig'] += [ ptf_si ]
Plots
Show code cell source
#> Initialize plot
fig, ax = plt.subplots(2, 3, figsize=(18, 12))
for iass in np.arange(len(mu)):
ax[0,0].plot(si[iass,iass]**.5, mu[iass], 'o', markersize=10, label=f'Asset {iass}')
for kptf, ptf in ptfs.items():
ax[0,0].plot(ptf['min_sig'], ptf['des_ret'], ptf['marker']+'-', label=kptf+': Efficient')
ax[0,0].set_ylim([min_mu-.2, max_mu+.2])
ax[0,0].set_xlabel("Std Deviation")
ax[0,0].set_ylabel("Expected return")
ax[0,0].grid()
ax[0,0].legend()
nptf = 0
for kptf, ptf in ptfs.items():
#> Weights
wmat = np.array(ptf['wmat'])
for i in np.arange(np.shape(wmat)[1]):
ax[nptf,1].plot(wmat[:,i], ptf['des_ret'], label=f"Asset{i}")
ax[nptf,1].set_xlabel("Weights")
ax[nptf,1].legend(); ax[nptf,1].grid()
ax[nptf,1].set_ylim([min_mu-.2, max_mu+.2])
#> Stacked - with expected return on y
cum = np.zeros((np.shape(wmat)[0], np.shape(wmat)[1]+1))
for i in np.arange(1, np.shape(cum)[1]):
cum[:,i] = cum[:,i-1] + wmat[:,i-1]
for i in np.arange(1, np.shape(cum)[1]):
ax[nptf,2].fill_betweenx(ptf['des_ret'], cum[:,i-1], cum[:,i], label=f"Asset{i-1}", alpha = .5)
ax[nptf,2].legend(); ax[nptf,2].grid() # loc='upper right'
ax[nptf,2].set_xlabel('Portfolio composition')
ax[nptf,2].set_ylim([min_mu-.2, max_mu+.2])
nptf += 1
plt.show()
18.3. Efficient frontier and CAPM without leverage and short-selling#
Show code cell source
for kptf, ptf in ptfs.items():
n_assets = len(ptf['mu'])
ptf['wmat'] = []
ptf['min_sig'] = []
ptf['des_ret'] = np.linspace(np.min(ptf['mu']), np.max(ptf['mu']), 30)
for desired_ret in ptf['des_ret']:
#> Constraints (just comment if you don't want some)
eq_cons = [
{'type': 'eq', 'fun': partial(eq_desired_return, mu=ptf['mu'], desired_ret=desired_ret)},
{'type': 'eq', 'fun': eq_weight_sum}, # sum(w) = 1 (fully invested)
]
eq_cons += [ {'type': 'ineq', 'fun': lambda x: x[i]} for i in np.arange(n_assets) ]
eq_cons += [ {'type': 'ineq', 'fun': lambda x: 1-x[i]} for i in np.arange(n_assets) ]
cost_fun = partial(ptf_var, sigma=ptf['si'])
x0 = np.zeros(n_assets); x0[0] = 1 # np.ones(3) / 3.
res = minimize(cost_fun, x0, constraints=eq_cons,)
# print(f"Desired Return: {desired_ret}, res: {res.x}")
ptf_si = np.sqrt(res.x @ ptf['si'] @ res.x)
ptf_mu = np.sum(res.x * ptf['mu'])
#> Store weights and min variance, for the desired expected return
ptf['wmat'] += [ res.x ]
ptf['min_sig'] += [ ptf_si ]
Plots.
Show code cell source
#> Initialize plot
fig, ax = plt.subplots(2, 3, figsize=(18, 12))
for iass in np.arange(len(mu)):
ax[0,0].plot(si[iass,iass]**.5, mu[iass], 'o', markersize=10, label=f'Asset {iass}')
for kptf, ptf in ptfs.items():
ax[0,0].plot(ptf['min_sig'], ptf['des_ret'], ptf['marker']+'-', label=kptf+': Efficient')
ax[0,0].set_ylim([min_mu-.2, max_mu+.2])
ax[0,0].set_xlabel("Std Deviation")
ax[0,0].set_ylabel("Expected return")
ax[0,0].grid()
ax[0,0].legend()
nptf = 0
for kptf, ptf in ptfs.items():
#> Weights
wmat = np.array(ptf['wmat'])
for i in np.arange(np.shape(wmat)[1]):
ax[nptf,1].plot(wmat[:,i], ptf['des_ret'], label=f"Asset{i}")
ax[nptf,1].set_xlabel("Weights")
ax[nptf,1].legend(); ax[nptf,1].grid()
ax[nptf,1].set_ylim([min_mu-.2, max_mu+.2])
#> Stacked - with expected return on y
cum = np.zeros((np.shape(wmat)[0], np.shape(wmat)[1]+1))
for i in np.arange(1, np.shape(cum)[1]):
cum[:,i] = cum[:,i-1] + wmat[:,i-1]
for i in np.arange(1, np.shape(cum)[1]):
ax[nptf,2].fill_betweenx(ptf['des_ret'], cum[:,i-1], cum[:,i], label=f"Asset{i-1}", alpha = .5)
ax[nptf,2].legend(); ax[nptf,2].grid() # loc='upper right'
ax[nptf,2].set_xlabel('Portfolio composition')
ax[nptf,2].set_ylim([min_mu-.2, max_mu+.2])
nptf += 1
plt.show()