MPT Part 3: Efficient Frontier

19 minute read

Published:

Modelling and visualisation on some of the financial engineering theorems using Jupyter Notebook. The mathematics of the theorems follow the book: Capinski, M. and Zastawniak, T. (2011). Mathematics for Finance. An Introduction to Financial Engineering (Second Edition).

Check the Jupyter Notebook in my Github repository Python-for-mathematical-finance , or run the code in Google Colab MPT Part 3 - Efficient Frontier.

Table of Contents

Efficient Frontier

import numpy as np
import sympy as sy
from scipy import stats
import pandas as pd
import pandas_datareader as pdr
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import plotly.express as px
import plotly.graph_objs as go
from tqdm.notebook import tqdm
tqdm.pandas()

Minimum variance line

Theoretical development

If the expected return $\mu$ is given, there exists a portfolio’s expected return $\mu_V=\mu$ and variance $\sigma_V < \sigma_{V’}$ for all the attainable portfolios $V’$ with $\mu_{V’}=\mu$. For $\mu \in \mathbb{R}$, such set of the portfolio ($\mu_V$, $\sigma_V$) form the minimum variance line, or the minimum variance frontier.

The portfolio with the smallest variance among attainable portfolios with expected return $\mu_V$ has weights:

\[\begin{equation} w = \frac{\lambda_1}{2}mC^{-1}+\frac{\lambda_2}{2}uC^{-1}, \end{equation}\]

where

\[\begin{equation} \begin{bmatrix} \lambda_1 \\\ \lambda_2 \end{bmatrix}=2\begin{bmatrix}mC^{-1}m^{T} & uC^{-1}m^{T} \\\ mC^{-1}u^{T} & uC^{-1}u^T\end{bmatrix}^{-1}\begin{bmatrix}\mu \\\ 1 \end{bmatrix}. \end{equation}\]

$w$ is the $n$ array of portfolio weights. $m$ is the $n$ array of stock returns. $u$ is a $n$ array of 1. $C$ is the $n \times n$ covariance matrix of the portfolio.

Proof

For $\mu \in \mathbb{R}$, to get the minimum variance $wCw^T$ subject to (1) $wm^T=\mu$ and (2) $wu^T=1$, we take

\[\begin{equation*} G(w, \lambda_1, \lambda_2) = wCw^T- \lambda_1(wm^T-\mu)-\lambda_2(wu^T-1), \end{equation*}\]

where $\lambda$ and $\mu$ are Lagrange multipliers. The partial derivatives of $G$ with respect to the weights $w_i$, $\mu1$, and $\mu2$ equated to zero give the necessary conditions for a minimum:

\[\begin{equation*} 2wC - \lambda_1m - \lambda_2u = 0 \\\ wm^T - \mu = 0 \\\ wu^T - 1 =0 \end{equation*}\]

The first equation implies:

\[\begin{equation*} 2w = \lambda_1mC^{-1}+\lambda_2uC^{-1} \end{equation*}\]

Substituting this into the other two constraints, we obtain a system of linear equations

\[\begin{equation*} \lambda_1mC^{-1}m^T+\lambda_2uC^{-1}m^T=2\mu \\\ \lambda_1mC^{-1}u^T+\lambda_2uC^{-1}u^T=2, \end{equation*}\]

and the coefficients matrix can be written as

\[\begin{equation*} M = \begin{bmatrix} mC^{-1}m^T & uC^{-1}m^T \\\ mC^{-1}u^T & uC^{-1}u^T \end{bmatrix}. \end{equation*}\]

Then we can solve the $\lambda_1$ and $\lambda_2$:

\[\begin{equation*} \begin{bmatrix} \lambda_1\\\ \lambda_2 \end{bmatrix} = 2M^{-1}\begin{bmatrix} \mu\\\ 1 \end{bmatrix}. \end{equation*}\]

Function for calculating the minimum variance line

Description of cal_mvl(eret_list, std_list, corr_list, port_eret, print_formula= False, return_type="weight_list"):

The function takes the portfolio’s stock returns, standard deviations, and correlation list, then calculate the portfolio weights for the minimum variance line. If the expected portfolio is given, the function will return can be minimum variance weights list (return_type="weight_list", default), print minimum variance portfolio return, variance and weights (return_type="full_print"), or return a dictionary of the portfolio return and variance (return_type="dictionary"). If the expected portfolio return is unknown, the function can print the mean-variance formula of minimum variance line (print_formula=True, default is False), and return the list of the coefficients (“constant”, “return-linear coefficient”, and “return-quadratic coefficient”).

Arguments

  • eret_list, std_list, corr_list: like what we did before, these three parameters are lists of stock returns, standard deviations, and correlations. The correlation list should follow the combination order from smaller distance between $i$ and $j$ to larger, for example, if $n = 4$:
\[\begin{equation*} \begin{bmatrix} \rho_{12} & \rho_{23} & \rho_{34} & \rho_{13} & \rho_{24} & \rho_{14} \end{bmatrix} \end{equation*}\]
  • port_eret: if the entry is a number representing the expected portfolio return, such as 0.2, the output can be weight list return_type="weight_list", full print of the calculation (return_type="full_print"), or a dictionary storing the results (return_type="dictionary"). If the entry is None, the function can print the formula for minimum variance line (print_formula=True) and return the list of coefficients (power increasing order).
def cal_mvl(eret_list, std_list, corr_list, port_eret, print_formula=False, return_type="weight_list"):
    
    cov_mat = []
    n = len(std_list)
    cal_mvl.n = n
    u = np.array([1] * n)
    for x in range(1,n+1):
        cxy = []
        for y in range(1,n+1):
            if x == y:   
                cxy.append(std_list[x-1]**2)
            elif x < y:
                idx = int((y-x-1)*n-(((y-x)*(y-x-1))/2)+x)
                cxy.append(corr_list[idx-1]*std_list[x-1]*std_list[y-1])
            elif x > y:
                idx = int((x-y-1)*n-(((x-y)*(x-y-1))/2)+y)
                cxy.append(corr_list[idx-1]*std_list[x-1]*std_list[y-1])
        cov_mat.append(cxy)
    
    cal_mvl.cov_mat = cov_mat
    M = [[np.dot(np.dot(np.array(eret_list),np.linalg.inv(cov_mat)),np.array(eret_list).transpose()),
          np.dot(np.dot(u,np.linalg.inv(cov_mat)),np.array(eret_list).transpose())],
         [np.dot(np.dot(np.array(eret_list),np.linalg.inv(cov_mat)),u.transpose()),
          np.dot(np.dot(u,np.linalg.inv(cov_mat)),u.transpose())]]
    m_inverse = np.linalg.inv(M)
    
    if port_eret:
        e_port_ret2 = np.array([port_eret,1]).transpose()
        lamb1 = np.dot(m_inverse,e_port_ret2)[0]
        lamb2 = np.dot(m_inverse,e_port_ret2)[1]
        
        weight_array = lamb1*np.dot(eret_list,np.linalg.inv(cov_mat))+lamb2*np.dot(u,np.linalg.inv(cov_mat))
        weight_list = list(weight_array)

        port_var = np.dot(np.dot(weight_list, cov_mat), np.array(weight_list).transpose())
        if return_type == "weight_list":
            return weight_list
        elif return_type == "full_print":
            weight_round = list(np.round(weight_array,3))
            print("Condition: the expected portfolio return is %s" % round(port_eret, 4))
            print("The minimum variance weights are: %s, sum to %s" % (weight_round, round(np.sum(weight_array),0)))
            print("The portfolio's variance is %s" % round(port_var,4))
            try:
                print("The portfolio's standard deviation is %s" % round(np.sqrt(port_var),4))
            except:
                pass
        elif return_type == "dictionary":
            vola_dict = dict()
            vola_dict["return"] = port_eret
            vola_dict["variance"] = port_var

            return vola_dict
        
    else:
        x, y = sy.symbols('x y')
        e_port_ret2 = sy.Matrix([[y],[1]])
        m_lamb = sy.Matrix(m_inverse)*sy.Matrix(e_port_ret2)
        lamb1 = m_lamb[0]
        lamb2 = m_lamb[1]
        weight_array = lamb1*sy.Matrix(eret_list).transpose()*sy.Inverse(sy.Matrix(cov_mat))\
                      +lamb2*sy.Matrix(u).transpose()*sy.Inverse(sy.Matrix(cov_mat))
        cal_mvl.weight_array = weight_array
        port_var = weight_array*sy.Matrix(cov_mat)*weight_array.transpose()

        # Calculate MVP for plot
        weight_array_mvp = np.dot(u,np.linalg.inv(cov_mat))/np.dot(np.dot(u,np.linalg.inv(cov_mat)),u.transpose())
        weight_list_mvp = list(weight_array_mvp)
        port_eret_mvp = np.dot(np.array(weight_list_mvp), np.array(eret_list).transpose())
        port_var_mvp = np.dot(np.dot(weight_list_mvp, cov_mat), np.array(weight_list_mvp).transpose())
        cal_mvl.port_eret_mvp = port_eret_mvp
        cal_mvl.port_var_mvp = port_var_mvp
        
        formula_left = sy.simplify(port_var[0])
        formula = sy.Poly(formula_left - x**2)
        by2 = formula.coeffs()[1]
        by1 = formula.coeffs()[2]
        cons = formula.coeffs()[3]
        
        if print_formula:
            str_formula = "The frontier is: σ\u00b2 = %s + %sμ + %sμ\u00b2" %(round(cons,3),round(by1,3),round(by2,3))
            print(str_formula.replace("+ -", "- "))
        else:
            pass
        coeff_list = [cons, by1, by2]
        return coeff_list

If the expected portfolio return is given, i.e. the parameter entry of port_eret is a number, cal_mvl will return for the portfolio with conditional minimum variance either the list of weights (return_type="weight_list", by default), or a dictionary containing the return and variance (return_type = "dictionary"), or just print the return, weights, variance and standard deviation (return_type = "full_print").

eret_list = [0.2, 0.13, 0.17]
std_list = [0.25, 0.28, 0.2]
corr_list = [0.3, 0, 0.15]
port_eret = 0.2

cal_mvl(eret_list, std_list, corr_list, port_eret, return_type="full_print")
Condition: the expected portfolio return is 0.2
The minimum variance weights are: [0.722, -0.208, 0.486], sum to 1
The portfolio's variance is 0.0444
The portfolio's standard deviation is 0.2107

If the expected portfolio is not given, i.e. the parameter entry is False, cal_mvl will compute the formula of the minimum variance line with “y” as the expected return and “x” as the standard deviation.

If keyword argument print_formula=True (by default is False), cal_mvl will print the formula of the minimum variance line and return a list of coefficients, which are “constant”, “y-linear coefficient”, and “y-quadratic coefficient”.

eret_list = [0.1, 0.15, 0.2]
std_list = [0.28, 0.24, 0.25]
corr_list = [-0.1, 0.2, 0.25]
port_eret = False 

cal_mvl(eret_list, std_list, corr_list, port_eret, print_formula=True)
The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

The minimum variance weights are [0.237392428474999, -2.88543916091178, 9.85134389116777]

Three securities visualisation (type 1): using two weights as parameters

There are two convenient ways to visualise all portfolios that can be constructed from the three securities. The following cell provide the first one using two of the three weights, $w_2$ and $w_3$, as parameters. The remaining weight $w_1=1-w_2-w_3$. The three points of the blue triangle are three situations where the portfolio only has one stock with 100% weight. The blue sides of the triangle represent the situation where the portfolio only contains two stocks. Points inside of the triangle, including the boundaries, represent the situation without short selling. The minimum variance line is the orange line with the equation printed above the figure. Here, the minimum variance line is linear because the linear relationship between the expected portfolio return and weights.

w2, w3, y = sy.symbols("w2 w3 y")
eqw2 = sy.Eq(cal_mvl.weight_array[1], w2)
eqw3 = sy.Eq(cal_mvl.weight_array[2], w3)
eqw2w3 = sy.Poly(sy.simplify(sy.solve(eqw2, y)[0]-sy.solve(eqw3, y)[0]))
str_eq = "w\u2083 = %sw\u2082 + %s" %(round(eqw2w3.coeffs()[0],3), round((eqw2w3.coeffs()[2])/(-eqw2w3.coeffs()[1]),3))
print("The minimum variance line is: "+ str_eq.replace("+ -", "- "))
print("The remaining weight w\u2081 = 1 - w\u2082 - w\u2083")

f = plt.figure(figsize=(7, 7))
ax = f.add_subplot(1, 1, 1)

x = np.linspace(-0.2, 1.2, 100)
y = (eqw2w3.coeffs()[0]*x + eqw2w3.coeffs()[2])/(-eqw2w3.coeffs()[1])
y1 = 1 - x
plt.plot(x, y, color=list(mcolors.TABLEAU_COLORS.values())[1], lw=3)
plt.plot(x, y1, color=list(mcolors.TABLEAU_COLORS.values())[0])
plt.scatter([0,0,1],[0,1,0])
plt.xlim(-0.2, 1.2)
plt.ylim(-0.2, 1.2)
plt.axhline(0, color=list(mcolors.TABLEAU_COLORS.values())[0])
plt.axvline(0, color=list(mcolors.TABLEAU_COLORS.values())[0])
plt.xlabel("Weight of the second stock (w\u2082)")
plt.ylabel("Weight of the third stock (w\u2083)")

ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')

plt.show()

The minimum variance line is: w₃ = -0.361w₂ + 2.050
The remaining weight w₁ = 1 - w₂ - w₃

Description of simulate_weights(repetition, eret_list, std_list, corr_list, short_selling=False, plot_scatter=False)

Like what we did, the function takes the portfolio’s return list, standard deviation list and correlation list, then generates random weights for repetition times. These weights are used to calculate portfolio’s return and standard deviation for plot. The return can be a sample scatter of the standard deviation and the expected return, or a pandas DataFrame containing the standard deviation, the expected return, the efficiency measure ($\frac{\mu}{\sigma}$), and the weight for the second security (the reason will be explained later).

Arguments

  • repetition: number of times for the random weight simulation. 10,000 times would return a pretty good plot.
  • short_selling=False: default is False, which means the weights will only be randomly generated within 0 to 1. If True, the weights will be generate following the standard normal distribution, then adjusted to sum to 1.
  • plot_scatter=False: default is False, which will return a a pandas DataFrame containing the standard deviation, the expected return, the efficiency measure ($\frac{\mu}{\sigma}$), and the weight for the second security. If True, the function will generate a scatter of the standard deviation and return. However, the plot_scatter=True is only used inside our later function for visualisation.
def simulate_weights(repetition, eret_list, std_list, corr_list, short_selling=False, plot_scatter=False):
        port_eret = False 
        cal_mvl(eret_list, std_list, corr_list, port_eret)
        random_list = []
        times = 1
        while times < repetition:
            random_dict = dict()
            if short_selling:
                weight_rand = np.array(1 + 1 * np.random.randn(cal_mvl.n))
            else:
                weight_rand = np.array(np.random.random(cal_mvl.n))
            weight_rand_list = list(weight_rand/np.sum(weight_rand))
            port_eret_rand = np.dot(np.array(weight_rand_list), np.array(eret_list).transpose())
            port_var_rand = np.dot(np.dot(weight_rand_list, cal_mvl.cov_mat), np.array(weight_rand_list).transpose())
            port_sd_rand = np.sqrt(port_var_rand)
            random_dict["Return"] = port_eret_rand
            random_dict["Standard deviation"] = port_sd_rand
            random_dict["Efficiency"] = (port_eret_rand) * (1/port_sd_rand)
            random_dict["Weight2"] = weight_rand_list[1]
            random_list.append(random_dict)
            times += 1
        df_random = pd.DataFrame(random_list)
        if plot_scatter:
            if short_selling:
                plt.scatter("Standard deviation", "Return", data=df_random, c="grey", marker="1", alpha=0.1)
            else:
                plt.scatter("Standard deviation", "Return", data=df_random, c="Efficiency", alpha=0.1)
        else:
            return df_random

Three securities visualisation (type 2): the Markowitz bullet

First, we can set the weight simulation for 10,000 times, the generate an interactive scatter using plotly. The graph is sometimes called the risk–expected return graph, and the shape is called the Markowitz bullet.

The first scatter represents the similar shape of all the attainable portfolios without considering short selling, which are the combinations of the three securities (orange diamonds). The lighter the color, the more efficient the portfolio is.

The second scatter allows short selling with outliers winsorised, and the sphere of the attainable portfolio becomes much wider, but still form the minimum variance line in bullet shape.

df_random = simulate_weights(10000, eret_list, std_list, corr_list, short_selling=False, plot_scatter=False)
fig = px.scatter(df_random, x="Standard deviation", y="Return", color="Efficiency")

fig.add_trace(go.Scatter(x=std_list, y=eret_list, mode='markers',
                         marker=dict(
                             color='Orange',
                             symbol='diamond',
                             size=12,
                             line=dict(
                                 color='MediumPurple',
                             width=2
                             )
                         ),
                         showlegend=True, name='Individual stock'))

fig.update_layout(legend=dict(x=0, y=1))
fig.update_traces(hovertemplate="<br>".join(["Return: %{y}","Standard deviation: %{x}"]))
dict_fig = fig.to_dict()
dict_fig["data"][0]["type"] = "scatter"
fig = go.Figure(dict_fig)
fig.show()

df_random2 = simulate_weights(10000, eret_list, std_list, corr_list, short_selling=True, plot_scatter=False)
df_random2 = df_random2[(np.abs(stats.zscore(df_random2["Standard deviation"])) < 0.2)]
fig = px.scatter(df_random2, x="Standard deviation", y="Return", color="Efficiency")
fig.add_trace(go.Scatter(x=std_list, y=eret_list, mode='markers',
                         marker=dict(
                             color='Orange',
                             symbol='diamond',
                             size=12,
                             line=dict(
                                 color='MediumPurple',
                             width=2
                             )
                         ),
                         showlegend=True, name='Individual stock'))

fig.update_layout(legend=dict(x=0, y=1))
fig.update_traces(hovertemplate="<br>".join(["Return: %{y}","Standard deviation: %{x}"]))
dict_fig = fig.to_dict()
dict_fig["data"][0]["type"] = "scatter"
fig = go.Figure(dict_fig)
fig.show()

Transformation: $w_2$-$w_3$ triangle to the Markowitz bullet

It is instructive to imagine how the above $w_2$-$w_3$ triangle is transformed into the risk–expected Markowitz bullet. The triangle is folded along the minimum variance line, being warped and stretched to attain the shape of the Markowitz bullet.

And here is the reason for the function simulate_weights() to return the weight of the second security in the DataFrame. The weight of the second security ($w_2$) is the additional dimension in the 3D interactive scatter below. Thus, we can clearly observe how the portfolio is formed from the three securities, how the $w_2$-$w_3$ triangle is warped and stretched into the Markowitz bullet, and how it display the bullet shape in the risk–expected 2D scatter. Because in three dimension, it is a bullet!

df_random3d = simulate_weights(10000, eret_list, std_list, corr_list, short_selling=False, plot_scatter=False)
fig = px.scatter_3d(df_random3d, x="Weight2", y="Standard deviation", z="Return", color="Efficiency")

weight2list = [0,1,0]
fig.add_trace(go.Scatter3d(x=weight2list, y=std_list, z=eret_list, 
                           mode='markers',
                           marker=dict(symbol='diamond',
                                       color="cornflowerblue"),
                           showlegend=True, 
                           name='Individual<br>stock'))

fig.update_layout(legend=dict(x=0, y=1))
fig.update_traces(hovertemplate="<br>".join(["Return: %{y}","Standard deviation: %{x}"]))
fig.show()

We can also add the efficient frontier and minimum variance portfolio in the same 3D scatter to better illustrate how the $w_2$-$w_3$ plane is folded along the minimum variance line.

Function for visualising the minimum variance and efficient frontier

Description of frontier_plot(eret_list, std_list, corr_list, simulate=(False,False), m_bullet=False)

Like previous function, this function take the portfolio’s stock returns, standard deviations and correlation list, and plot the equation of the minimum variance and efficient frontier (in solid blue line). The function also allows the following to appear in the same figure: (1) weight simulation scatter for both situations with and without short selling, and (2) the markowitz bullet line from each of the two securities.

Arguments

  • simulate=(False,False): inherently is simulate=(number1, number2) (default: False, False). The number1 randomly picks positive weights (from 0 to 1, sum to 1), meaning that short selling is not allowed. The number2 randomly picks weights (either positive or negative, sum to 1) based on the standard normal distribution $N(0,1)$, meaning that short selling is allowed

  • m_bullet=False: default is False. If True, will plot the markowitz bullet line from each of the two securities.

def frontier_plot(eret_list, std_list, corr_list, simulate=(False,False), m_bullet=False):
    
    f = plt.figure(figsize=(10,8))
    ax = f.add_subplot(1, 1, 1)
    
    port_eret = False  
    cal_mvl(eret_list, std_list, corr_list, port_eret)
    def mvl_curve(eret_list, std_list, corr_list, port_eret, print_formula=False, mark_bullet=False):
        coeff_list = cal_mvl(eret_list, std_list, corr_list, port_eret)
        cons, by1, by2 = coeff_list[0], coeff_list[1], coeff_list[2]
        str_formula = "The frontier is: σ\u00b2 = %s + %sμ + %sμ\u00b2" %(round(cons,3),round(by1,3),round(by2,3))
        if print_formula==True:
            print(str_formula.replace("+ -", "- "))
    
        x = np.linspace(0, max(std_list)*(4/3), 400)
        y = np.linspace(min(eret_list)-np.abs((2/3)*max(eret_list)), max(eret_list)+np.abs((2/3)*max(eret_list)), 400)
        x, y = np.meshgrid(x, y)

        new_formula = -x**2 + by2*y**2 + by1*y + cons
        masked_up = y < cal_mvl.port_eret_mvp
        masked_down = y > cal_mvl.port_eret_mvp
        if mark_bullet:
            plt.contour(x, y, new_formula, [0], linestyles='-', colors='orange', linewidths=0.8)

        else:
            plt.contour(np.ma.masked_where(masked_up,x), np.ma.masked_where(masked_up,y), np.ma.masked_where(masked_up,new_formula), [0], colors='deepskyblue', linestyles='-', linewidths=2)
            plt.contour(np.ma.masked_where(masked_down,x), np.ma.masked_where(masked_down,y), np.ma.masked_where(masked_down,new_formula), [0], colors='deepskyblue', linestyles='--', linewidths=2)
    
    mvl_curve(eret_list, std_list, corr_list, port_eret, print_formula=True, mark_bullet=False)
    
    if simulate[1]:
        simulate_weights(simulate[1], eret_list, std_list, corr_list, short_selling=True, plot_scatter=True)
    if simulate[0]:
        simulate_weights(simulate[0], eret_list, std_list, corr_list, short_selling=False, plot_scatter=True)
        
    plt.scatter(std_list, eret_list, color="orange", alpha=0.7)
    plt.scatter(np.sqrt(np.abs(cal_mvl.port_var_mvp)), cal_mvl.port_eret_mvp, marker='v', color="red")

    if m_bullet:
        for distance in range(1, len(eret_list)):
            i = 1
            next_i = i + distance
            while next_i <= len(eret_list):
                ret_new = [eret_list[i-1], eret_list[next_i-1]]
                std_new = [std_list[i-1], std_list[next_i-1]]
                corr_idx = (np.abs(i-next_i)-1)*len(eret_list)-((i-next_i)**2-np.abs(i-next_i))/2+min(i,next_i)-1
                corr_new = [corr_list[int(corr_idx)]]               
                mvl_curve(ret_new, std_new, corr_new, port_eret, mark_bullet=m_bullet)     
                i += 1
                next_i = i + distance
    
    plt.xlim([0, max(std_list)*(4/3)])
    plt.ylim([min(eret_list)-np.abs((2/3)*max(eret_list)), max(eret_list)+np.abs((2/3)*max(eret_list))])
    plt.xlabel("Standard Deviation (σ)")
    plt.ylabel("Expected Return (μ)")
    ax.spines['left'].set_position('zero')
    ax.spines['bottom'].set_position('zero')
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    ax.xaxis.set_ticks_position('bottom')
    ax.yaxis.set_ticks_position('left')
    plt.show()

With only three entries, frontier_plot can generate a plot of standard deviation and return including the scatter of individual stocks (orange dots), the global minimum variance portfolio (red marker), and the minimum variance line (blue solid line is the efficient frontier).

frontier_plot(eret_list, std_list, corr_list, m_bullet=False)
frontier_plot(eret_list, std_list, corr_list, m_bullet=True)
The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

Furthurmore, we can set the number of random simulations for portfolio weights and plot the scatter of these mean-variance pairs, by adding number of random simulations to the keyword argument simulate=(number1, number2) (default: False, False). The number1 randomly picks positive weights (from 0 to 1, sum to 1), meaning that short selling is not allowed. The number2 randomly picks weights (either positive or negative, sum to 1) based on the standard normal distribution $N(0,1)$, meaning that short selling is allowed.

For instance:

  1. simulate=(10000,False) will only plot 10,000 randomly weighted portfolios with positive weights. The lighter the color, the more efficient the portfolio is.

  2. simulate=(False,10000) will only plot 10,000 randomly weighted portfolios with positive or negative weights, considering the short selling exists.

  3. simulate=(10000,10000) will plot 10,000 randomly weighted portfolios with positive weights in color gradients, and 10,000 randomly weighted portfolios with positive or negative weights in grey.

frontier_plot(eret_list, std_list, corr_list, simulate=(10000,False), m_bullet=True)
frontier_plot(eret_list, std_list, corr_list, simulate=(False,10000), m_bullet=True)
frontier_plot(eret_list, std_list, corr_list, simulate=(10000,10000), m_bullet=True)
The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

The frontier is: σ² = 0.237 - 2.885μ + 9.851μ²

Example with sample data

Description of cal_sample_mvl(n_sample, dataframe, port_eret=False, return_type="weight_list", sym_plot=True, simulate=(False,False), m_bullet=False)

This function can do all the works of previous functions, whether calculating minimum variance weights or equation, or generate line graph for the equation and simulation scatter, but with real world data.

Arguments

  • n_sample (type: int): number of sample that are randomly selected from the data set. If exceed the index range of data set, n_sample will equal the length of the data set minus 10 (or equal 1 if still out of index range).
  • dataframe (type: Pandas DataFrame). Below is an example DataFrame using the 30 industry portfolios monthly return data (June 2017 - April 2022) from Fama French library by pandas_datareader.
  • port_eret=False and return_type="weight_list": see cal_mvl().
  • sym_plot=True: if True (default), will return the plot. If False, will return minimum variance weights (when port_eret has assigned a value) or the formula of minimum variance line (port_eret=False, default).
  • simulate=(False,False), m_bullet=False: see frontier_plot().
def cal_sample_mvl(n_sample, dataframe, port_eret=False, return_type="weight_list", sym_plot=True, simulate=(False,False), m_bullet=False):
    df = dataframe 
    if n_sample < len(df.columns) and n_sample > 0:
        num = n_sample
    else:
        try:
            num = len(df.columns) - 10
        except:
            num = 1
        print("[Error] number out of range. Plot default sample.")
    
    df_sample = pd.concat([df["yrm"], df.loc[:,df.columns!="yrm"].sample(n=num,axis='columns',replace=False)], axis=1)
    corr_list = []
    for distance in range(1, len(df_sample.columns)-1):
        i = 1
        next_i = i + distance
        while next_i <= len(df_sample.columns)-1:
            correlation = df_sample[df_sample.columns[i]].corr(df_sample[df_sample.columns[next_i]])
            corr_list.append(correlation)
            i += 1
            next_i = i + distance
    eret_list = [df_sample[df_sample.columns[i]].mean() for i in range(1, len(df_sample.columns))]
    std_list = [df_sample[df_sample.columns[i]].std() for i in range(1, len(df_sample.columns))]
    if port_eret:
        return cal_mvl(eret_list, std_list, corr_list, port_eret, return_type=return_type)
    else:
        if sym_plot:
            return frontier_plot(eret_list, std_list, corr_list, simulate=simulate, m_bullet=m_bullet)
        else:
            return cal_mvl(eret_list, std_list, corr_list, port_eret=False, print_formula=True)
  
df = pdr.DataReader('30_Industry_Portfolios', 'famafrench')[0].progress_apply(lambda x: x*0.01)
df["yrm"] = df.index
port_eret = 0.2
cal_sample_mvl(10, df, port_eret, return_type="full_print", sym_plot=False)
Condition: the expected portfolio return is 0.2
The minimum variance weights are: [-2.313, 4.288, -9.424, -10.191, -7.877, 2.97, 10.641, -4.2, 13.823, 3.284], sum to 1
The portfolio's variance is 0.3364
The portfolio's standard deviation is 0.58
cal_sample_mvl(10, df, sym_plot=False)
The frontier is: σ² = 0.002 - 0.176μ + 7.576μ²

The minimum variance weights are [0.00231063434901240, -0.176171064803407, 7.57623660449877]
cal_sample_mvl(5, df, sym_plot=True)
cal_sample_mvl(5, df, sym_plot=True, simulate=(10000,False), m_bullet=True)
cal_sample_mvl(5, df, sym_plot=True, simulate=(10000,10000), m_bullet=False)
The frontier is: σ² = 0.004 - 0.696μ + 46.675μ²

The frontier is: σ² = 0.003 - 0.247μ + 11.317μ²

The frontier is: σ² = 0.044 - 10.273μ + 620.085μ²