Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mobook
GitHub Repository: mobook/mo-book
Path: blob/main/notebooks/05/03-markowitz-portfolio.ipynb
663 views
Kernel: Python 3 (ipykernel)

5.3 Markowitz portfolio optimization

Preamble: Install Pyomo and a solver

This cell selects and verifies a global SOLVER for the notebook. If run on Google Colab, the cell installs Pyomo and ipopt, then sets SOLVER to use the ipopt solver. If run elsewhere, it assumes Pyomo and the Mosek solver have been previously installed and sets SOLVER to use the Mosek solver via the Pyomo SolverFactory. In both cases it then verifies that SOLVER is available.

import sys, os if 'google.colab' in sys.modules: %pip install idaes-pse --pre >/dev/null 2>/dev/null !idaes get-extensions --to ./bin os.environ['PATH'] += ':bin' solver = "ipopt" else: solver = "mosek_direct" import pyomo.environ as pyo SOLVER = pyo.SolverFactory(solver) assert SOLVER.available(), f"Solver {solver} is not available."
import numpy as np import matplotlib.pyplot as plt

Problem description and model formulation

A canonical stochastic optimization problem is the so-called portfolio selection problem, also known as Markowitz portfolio optimization. Assume that an investor has an initial capital CC that she wants to invest in nn possible risky assets, each of them with an unknown return rate rir_i, i=1,,ni=1,\dots,n, or in another risk-free asset with a guaranteed return rate RR. Let xx be the vector whose ii-th component xix_i describes the amount invested in asset ii and x~\tilde{x} the amount invested in the risk-free asset. We consider a stochastic model in which the return of the nn risky assets is then a random vector rr with known expected values μ=Er\mu = \mathbb E r and covariance

Σ=E[(rμ)(rμ)].\Sigma = \mathbb{E} [ (r-\mu)(r-\mu)^\top].

The investment return y=Rx~+rxy = R \tilde{x} + r^\top x then becomes also a random variable with mean

Ey=Rx~+Erx=Rx~+μx\mathbb{E} y = R \tilde{x} + \mathbb{E} r^\top x = R \tilde{x} + \mu^\top x

and variance

Var(y)=E(yEy)2=xΣx.\mathrm{Var}(y) = \mathbb{E}(y-\mathbb{E}y)^2 = x^\top \Sigma x.

The variance of the investment return is a possible way to quantify the risk of the investment xx.

The investor needs to select a portfolio that achieves a good compromise between risk and expected return. One could try to maximize the expected return Ey\mathbb{E} y subject to an upper bound on the tolerable risk, obtaining the following optimization problem:

maxRx~+μxs.t.i=1nxi+x~=CxΣxγx~0xi0i=1,,n.\begin{align*} \max \quad & R \tilde{x} + \mu^\top x \\ \text{s.t.}\quad & \sum_{i=1}^n x_i + \tilde{x} = C \\ & x^\top \Sigma x \leq \gamma \\ & \tilde{x} \geq 0 \\ & x_i \geq 0 & \forall \, i=1,\dots,n. \end{align*}

The first constraint describes the fact that the total amount invested must be equal to the initial capital. The second constraint ensures that the variance of the chosen portfolio is upper bounded by a parameter γ\gamma, which captures the risk the investor is willing to take. The last non-negativity constraint excludes the possibility of short-selling.

One can easily show that the quadratic constraint xΣxγx^\top \Sigma x \leq \gamma is convex in xx due to the fact that Σ\Sigma is positive semi-definite, being a covariance matrix. Therefore, the optimization problem is convex. Let us implement it in Pyomo and solve it.

def markowitz(gamma, mu, Sigma): m = pyo.ConcreteModel("Markowitz portfolio optimization") m.xtilde = pyo.Var(domain=pyo.NonNegativeReals) m.x = pyo.Var(range(n), domain=pyo.NonNegativeReals) @m.Objective(sense=pyo.maximize) def objective(m): return mu @ m.x + R * m.xtilde @m.Constraint() def bounded_variance(m): return (m.x @ (Sigma @ m.x)) <= gamma @m.Constraint() def total_assets(m): return sum(m.x[i] for i in range(n)) + m.xtilde == C return m

We now calculate the optimal investment strategy for a portfolio with n=3n=3 assets, with expected returns μ=(1.2,1.1,1.3)\mu = (1.2, 1.1, 1.3) and a predefined covariance matrix. We set the risk parameter γ\gamma to 11 and the risk-free return rate RR to 1.011.01.

# Specify the initial capital, the risk threshold, and the guaranteed return rate. C = 1 gamma = 1 R = 1.01 # Specify the number of assets, their expected return, and their covariance matrix. n = 3 mu = np.array([1.2, 1.1, 1.3]) Sigma = np.array([[1.5, 0.5, 2], [0.5, 2, 0], [2, 0, 5]]) # Check that Sigma is semi-definite positive assert np.all(np.linalg.eigvals(Sigma) >= 0) # When changing the matrix Sigma, make sure to input a semi-definite positive matrix. # The easiest way to generate a random covariance matrix is as follows: # first generate a random m x m matrix A and than take the matrix A A^T # which is always semi-definite positive by construction. # # m = 3 # A = np.random.rand(m, m) # Sigma = A.T @ A m = markowitz(gamma, mu, Sigma) SOLVER.solve(m) print( f"Optimal solution: xtilde = {m.xtilde.value:.3f}, x_1 = {m.x[0].value:.3f}, x_2 = {m.x[1].value:.3f}, x_3 = {m.x[2].value:.3f}" ) print(f"Optimal investment return: {m.objective():.2f}")
Optimal solution: xtilde = 0.158, x_1 = 0.561, x_2 = 0.142, x_3 = 0.139 Optimal investment return: 1.17

The next plot shows how the objective function changes as the risk threshold parameter γ\gamma varies.

gamma_values = np.linspace(0, 5, 100) objective = [] plt.rcParams.update({"font.size": 14}) for gamma in gamma_values: m = markowitz(gamma, mu, Sigma) SOLVER.solve(m) objective.append(round(m.objective(), 3)) plt.plot(gamma_values, objective, color=plt.cm.tab20c(0)) plt.xlabel(r"Risk threshold $\gamma$") plt.ylabel("Optimal objective value") plt.tight_layout() plt.show()
Image in a Jupyter notebook