Investment Management with Python - 3

Portfolio Optimization and Efficient Frontier

By Vamshi Jandhyala in finance

October 26, 2020

Portfolio Return

If $w_A$ is the weight of asset $A$ and $w_B$ is the weight of asset $B$ in the portfolio, then the return of the portfolio is given by $w_AR_A + w_BR_B$ where $R_A$ is the return of asset $A$ and $R_B$ is the return of asset $B$.

In the general case where $w_1,w_2, \dots, w_n$ and $R_1,R_2, \dots, R_n$ are the weights and returns of the assets in a portfolio, the portfolio return is $\sum_{i=1}^n w_iR_i = \bf{w}^T\bf{R}$. Here

$$ \bf{w} = \begin{bmatrix} w_1\
w_2\
\vdots\
w_n \end{bmatrix} , \bf{R} = \begin{bmatrix} R_1\
R_2\
\vdots\
R_n \end{bmatrix} $$

Code for calculating portfolio return

def portfolio_return(weights, returns):
    """
    Computes the return on a portfolio from constituent returns and weights
    weights are a numpy array or Nx1 matrix and returns are a numpy array or Nx1 matrix
    """
    return weights.T @ returns

Portfolio Variance

If $w_A$ is the weight of asset $A$ and $w_B$ is the weight of asset $B$ in the portfolio, then the variance of the portfolio is given by

$$ \begin{align*} Var(w_AR_A + w_BR_B) = w_A^2Var(R_A) + w_B^2Var(R_B) + \
2w_Aw_B \rho CoV(R_A,R_B) \end{align*} $$

where $R_A$ is the return of asset $A$ and $R_B$ is the return of asset $B$ and $\rho$ is the correlation between returns of assets $A$ and $B$.

In the general case where $w_1,w_2, \dots, w_n$ are the weights and $\sigma_{ij}, i,j \in {1,2,\dots, n}$ are the covariances of the pairwise returns of assets in the portfolio, the portfolio variance is given by $\bf{w}^T\Sigma\bf{w}$. Here

$$ \bf{w} = \begin{bmatrix} w_1\
\vdots\
w_n \end{bmatrix} , \bf{\Sigma} = \begin{bmatrix} \sigma_{1}^2 & \dots & \sigma_{1n}\
\vdots & \ddots & \vdots\
\sigma_{n1} & \dots & \sigma_{n}^2 \end{bmatrix} $$

We have $\sigma_{ii} = \sigma_{i}^2$ and $\sigma_{ij} = \rho_{ij} \sigma_{i} \sigma_{j}$ and $\rho_{ij}$ is the correlation between returns of asset $i$ and asset $j$.

Code for calculating portfolio variance

def portfolio_vol(weights, covmat):
    """
    Computes the vol of a portfolio from a covariance matrix and constituent weights
    weights are a numpy array or N x 1 maxtrix and covmat is an N x N matrix
    """
    return (weights.T @ covmat @ weights)**0.5

Magic of Diversification

The magic of diversification is simply that one can take two assets and mix them in a certain ratio, and end up with a portfolio that has a lower volatility than either one of them.

If the two assets are perfectly correlated (i.e. correlation is $1$) we get a straight line in the mean variance plane.If the correlation starts to drop off, we get a parabola.

Efficient frontier

When we have only two assets, all the portfolios formed by combining the two assets lie on a curve in the mean variance plane.

When we have three assets, we now have an entire region of portfolios. Every single portfolio is a point within that region. Conversely, every point on that region, can be traced back to some combination of weights, to these underlying assets.

One would never hold any portfolio in the interior of that region because there’s always at least two portfolios that are better. One is the one going straight up, which is going to give you a higher return for the same level of volatility. The other is the one on the extreme left at the edge of that region where for the same return you have lower volatility.

In other words, all of the portfolios on the efficient frontier are portfolios that you cannot improve on without changing either the volatility or the return.

Out of all that entire region of portfolios that one could possibly hold the only ones that are of interest to a rational investor are the ones sitting on the edge of the region, that line is called the efficient frontier. The efficient frontier, are the only portfolios that a mean-variance investor should be interested in. That’s really the core insight of modern portfolio theory.

Code for plotting the Efficient Frontier (two Assets)

def plot_ef2(n_points, er, cov):
    """
    Plots the 2-asset efficient frontier
    """
    if er.shape[0] != 2 or er.shape[0] != 2:
        raise ValueError("plot_ef2 can only plot 2-asset frontiers")
    weights = [np.array([w, 1-w]) for w in np.linspace(0, 1, n_points)]
    rets = [portfolio_return(w, er) for w in weights]
    vols = [portfolio_vol(w, cov) for w in weights]
    ef = pd.DataFrame({
        "Returns": rets, 
        "Volatility": vols
    })
    return ef.plot.line(x="Volatility", y="Returns", style=".-")

Efficient Frontier (two or more assets)

The portfolio comprising only of the asset with the lowest return lies on the efficient frontier because there is no other way to get a lower return than that.

Similarly, the portfolio comprising only of the asset with the highest return lies on the efficient frontier.

We now take the space between the minimum return and the maximum return and split it into a grid. Then at every point on that grid, we can run the optimizer to try and find the set of weights that gives the portfolio that minimizes the volatility for that level of return. Here is the optimization procedure

QUADRATIC FORM

$$ Minimize: \frac{1}{2}\bf{w^T\Sigma w} $$

CONSTRAINTS

$$ \bf{w^TR = r_0} \
\bf{w^T 1 = 1} \
\bf{w \geq 0} $$

Quadratic form is the input that is most suitable for a quadratic optimizer.

The objective function is minimizing the variance.

The first constraint is that the return must be at that certain level. We are going to run the optimization procedure over and over again across the grid of return values from the minimum return to the maximum so that we can plot each point along the curve.

The second constraint is that typically we want is that the weights all add up to one i.e. we are fully investing the money.

The third constraint is that, we want all the weights to be greater than zero. So in other words, no shorting.

Code for plotting efficient frontier (two or more assets)

from scipy.optimize import minimize

def minimize_vol(target_return, er, cov):
    """
    Returns the optimal weights that achieve the target return
    given a set of expected returns and a covariance matrix
    """
    n = er.shape[0]
    init_guess = np.repeat(1/n, n)
    bounds = ((0.0, 1.0),) * n # an N-tuple of 2-tuples!
    # construct the constraints
    weights_sum_to_1 = {'type': 'eq',
                        'fun': lambda weights: np.sum(weights) - 1
    }
    return_is_target = {'type': 'eq',
                        'args': (er,),
                        'fun': lambda weights, er: target_return - portfolio_return(weights,er)
    }
    weights = minimize(portfolio_vol, init_guess,
                       args=(cov,), method='SLSQP',
                       options={'disp': False},
                       constraints=(weights_sum_to_1,return_is_target),
                       bounds=bounds)
    return weights.x

def optimal_weights(n_points, er, cov):
    """
    Returns a list of weights that represent a grid of n_points on the efficient frontier
    """
    target_rs = np.linspace(er.min(), er.max(), n_points)
    weights = [minimize_vol(target_return, er, cov) for target_return in target_rs]
    return weights

def plot_ef(n_points, er, cov, style='.-', legend=False):
    """
    Plots the multi-asset efficient frontier
    """
    weights = optimal_weights(n_points, er, cov)
    rets = [portfolio_return(w, er) for w in weights]
    vols = [portfolio_vol(w, cov) for w in weights]
    ef = pd.DataFrame({
        "Returns": rets, 
        "Volatility": vols
    })
    ax = ef.plot.line(x="Volatility", y="Returns", style=style, legend=legend)
    return ax

Capital Market Line and Maximum Sharpe Ratio Portfolio

When we look at efficient portfolios, we are trying to find the set of portfolios that have the highest possible return for a given volatility level or given volatility target. When there are only risky assets, the usual shape of the Markowitz efficient frontier is a curve, the upper side of the envelope of all feasible portfolios. When we introduce a risk-free asset, we have more feasible portfolios. In particular any portfolio that lies on a straight line, that goes from the risk-free asset to any risky portfolio, is also a feasible portfolio. The efficient frontier changes shape and becomes a straight line.

The portfolios that are on those straight line are simply holdings of the risk-free asset on the one hand, and the risky portfolio on the other hand. Because we want to increase the return per unit of risk, what we’re trying to do is maximize the slope of that straight line. The way to maximize the slope of that straight line is to increase it all the way until we get it to the tangency point with respect to the Markowitz Efficient Frontier made entirely of risky assets.If we increase it a little further, then we are not looking at feasible portfolios simply because there’s no risky portfolio that can be combined with the risk-free asset to generate this portfolio. The best we can do is to increase the slope of that line all the way until there’s only one intersection point. That straight line is known as the capital market line, and the name we give to that very particular portfolio is the tangency portfolio.The tangency portfolio is also called the maximum Sharpe ratio portfolio. The reason it is the maximum Sharpe ratio portfolio is because the slope of that line is given by the Sharpe ratio of the portfolio. By increasing the slope, we are essentially maximizing the Sharpe ratio. Maximum Sharpe ratio portfolios are very important because all investors are actually optimally choosing to hold a portfolio on the capital market line i.e. a combination of that maximum Sharpe ratio portfolio and the risk-free asset.

The tangency portfolio is the portfolio that maximizes the sharpe ratio

$$ SR_p = \frac{\mu_p - r_f}{\sigma_p} = \frac{\sum_{i=1}^N w_i\mu_i - r_f}{\sqrt{\sum_{i=1}^N w_iw_j\rho_{ij}\sigma_i\sigma_j}} $$

Properties of Maximum Sharpe Ratio portfolio

There’s lot of things that are nice about the maximum Sharpe ratio portfolio. It is the portfolio that give you the highest reward per unit of risk, which makes it very compelling and very attractive from an intuitive standpoint.

There’s also another important property which is perhaps less well known. If a factor model is used to decompose the risk of any given portfolio in terms of specific risks and systematic risk, It can be shown that the maximum Sharpe ratio portfolio contains no zero exposure to specific risks. It’s only systematic risk. That’s nice because specific risk tends not to be rewarded because it can be diversified away. By diversifying away anything that’s not rewarded, the maximum Sharpe ratio portfolio does a very good job at maximizing the reward per unit of risk.

Code for identifying the Maximum Sharpe Ratio portfolio

def msr(riskfree_rate, er, cov):
    """
    Returns the weights of the portfolio that gives you the maximum sharpe ratio
    given the riskfree rate and expected returns and a covariance matrix
    """
    n = er.shape[0]
    init_guess = np.repeat(1/n, n)
    bounds = ((0.0, 1.0),) * n # an N-tuple of 2-tuples!
    # construct the constraints
    weights_sum_to_1 = {'type': 'eq',
                        'fun': lambda weights: np.sum(weights) - 1
    }
    def neg_sharpe(weights, riskfree_rate, er, cov):
        """
        Returns the negative of the sharpe ratio
        of the given portfolio
        """
        r = portfolio_return(weights, er)
        vol = portfolio_vol(weights, cov)
        return -(r - riskfree_rate)/vol

    weights = minimize(neg_sharpe, init_guess,
                       args=(riskfree_rate, er, cov), method='SLSQP',
                       options={'disp': False},
                       constraints=(weights_sum_to_1,),
                       bounds=bounds)
    return weights.x

Challenges with Markowitz optimization

Estimation error is the key challenge in portfolio optimization. If you feed an optimizer with parameters that are severely mis-estimated, with lot of estimation errors embedded in them, you’re going to get a portfolio that’s not going to be a very meaningful portfolio. In particular, you’re going to get extreme portfolios with very severe and very strong allocation in some assets, and very severe and very strong negative allocation in other assets.

The problem is very severe because optimizers tend to act as error maximizing machines. In particular, what we’re saying is in an optimization process, typically, the asset that gets the largest allocation is typically not the asset that maximizes return for the investor but the asset that suffers from the largest amount of estimation risk.

The problem is particularly important when it comes to expected return estimate. The expected return estimates are much harder to obtain with a good degree of accuracy compared to variance-covariance matrix estimates.

The sample-based expected return parameter estimates are very noisy, not very reliable. In practice investors focus on the only portfolio on the efficient frontier for which no expected return parameter is needed. That portfolio is known as the global minimum variance portfolio. That portfolio has become extremely popular in investment management in equity space and multi-asset contexts as well.

The optimization procedure for identifying the global minimum variance portfolio is as follows:

QUADRATIC FORM

$$ Minimize: \frac{1}{2}\bf{w^T\Sigma w} $$

CONSTRAINTS

$$ \bf{w^T 1 = 1} \
\bf{w \geq 0} $$

The success of the global minimum variance portfolio in practice is due to the fact that we are trying to minimize variance without any expected return targets. That’s good because we don’t need to rely on expected return estimates, which again, are very noisy.

Code for calculating Global Minimum Volatility portfolio

def gmv(cov):
    """
    Returns the weights of the Global Minimum Volatility portfolio
    given a covariance matrix
    """
    n = cov.shape[0]
    return msr(0, np.repeat(1, n), cov)