18. Modern Portfolio Theory and Capital Asset Pricing Model#

Hide 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.

Hide 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,

\[x_1 = \sum_{i=1}^{N} x_0 w_i \left( 1 + r_i \right) \ .\]

Not-fully invested portfolio can be represented with an asset \(0\) (cash with zero return). The return is defined as

\[r = \frac{x_1 - x_0}{x_0} = \frac{x_1}{x_0} - 1 = \sum_{i=1}^{N} w_i ( 1 + r_i ) - 1 \ .\]

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

\[r = \sum_{i=1}^{N} w_i r_i = \mathbf{w}^T \mathbf{r} \ .\]

Expected value and variance of the return of the portfolio. The expected value and the variance of the return thus read

\[\mu = \mathbb{E}[ r ] = \mathbb{E}[ \mathbf{w}^T \mathbf{r} ] = \mathbf{w}^T \boldsymbol\mu \ .\]

and

\[\begin{split}\begin{aligned} \sigma^2 & = \mathbb{E}[ ( r - \mu )^2 ] = \\ & = \mathbb{E}\left[ ( ( \mathbf{r} -\boldsymbol\mu )^T \mathbf{w})^T ( ( \mathbf{r} -\boldsymbol\mu )^T \mathbf{w} ) \right] = \\ & = \mathbf{w}^T \mathbb{E} \left[ ( \mathbf{r} -\boldsymbol\mu )( \mathbf{r} -\boldsymbol\mu )^T \right] \mathbf{w} = \mathbf{w}^T \boldsymbol\sigma^2 \mathbf{w} \ . \end{aligned}\end{split}\]

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

\[\begin{split}\begin{aligned} \mathbf{w}^* = \text{argmin}_{\mathbf{w}} \sigma^2 \quad \text{ s.t.} \quad & \mathbf{w}^T \boldsymbol{\mu} = \overline{{\mu}} \\ & \text{other constraints} \ , \end{aligned}\end{split}\]

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.

Hide 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#

Hide 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

Hide 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()
../../_images/c6a7f8f2bf305499ff2f3be8ea0f6426746cd96fd1e63d0c6f3a94c9c9dfe541.png

18.3. Efficient frontier and CAPM without leverage and short-selling#

Hide 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.

Hide 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()
../../_images/64480479e6b1c002774d6e96fe610d755e01f57267e929c32af505168104d5af.png