LaTeX macros (hidden cell) $ \newcommand{\Q}{\mathcal{Q}} \newcommand{\ECov}{\boldsymbol{\Sigma}} \newcommand{\EMean}{\boldsymbol{\mu}} \newcommand{\EAlpha}{\boldsymbol{\alpha}} \newcommand{\EBeta}{\boldsymbol{\beta}} $
import sys
import os
import re
import glob
import datetime as dt
import numpy as np
import pandas as pd
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
from mosek.fusion import *
from notebook.services.config import ConfigManager
from portfolio_tools import data_download, DataReader
# Version checks
print(sys.version)
print('matplotlib: {}'.format(matplotlib.__version__))
# Jupyter configuration
c = ConfigManager()
c.update('notebook', {"CodeCell": {"cm_config": {"autoCloseBrackets": False}}})
# Numpy options
np.set_printoptions(precision=5, linewidth=120, suppress=True)
# Pandas options
pd.set_option('display.max_rows', None)
# Matplotlib options
plt.rcParams['figure.figsize'] = [12, 8]
plt.rcParams['figure.dpi'] = 200
3.6.9 (default, Jan 26 2021, 15:33:00) [GCC 8.4.0] matplotlib: 3.3.4
In this example, the input data is given. It consists of the vector $\EMean$ of expected returns, and the covariance matrix $\ECov$.
# Linear return statistics on the investment horizon
mu = np.array([0.07197349, 0.15518171, 0.17535435, 0.0898094 , 0.42895777, 0.39291844, 0.32170722, 0.18378628])
Sigma = np.array([
[0.09460323, 0.03735969, 0.03488376, 0.03483838, 0.05420885, 0.03682539, 0.03209623, 0.03271886],
[0.03735969, 0.07746293, 0.03868215, 0.03670678, 0.03816653, 0.03634422, 0.0356449 , 0.03422235],
[0.03488376, 0.03868215, 0.06241065, 0.03364444, 0.03949475, 0.03690811, 0.03383847, 0.02433733],
[0.03483838, 0.03670678, 0.03364444, 0.06824955, 0.04017978, 0.03348263, 0.04360484, 0.03713009],
[0.05420885, 0.03816653, 0.03949475, 0.04017978, 0.17243352, 0.07886889, 0.06999607, 0.05010711],
[0.03682539, 0.03634422, 0.03690811, 0.03348263, 0.07886889, 0.09093307, 0.05364518, 0.04489357],
[0.03209623, 0.0356449 , 0.03383847, 0.04360484, 0.06999607, 0.05364518, 0.09649728, 0.04419974],
[0.03271886, 0.03422235, 0.02433733, 0.03713009, 0.05010711, 0.04489357, 0.04419974, 0.08159633]
])
The optimization problem we would like to solve is $$ \begin{array}{lrcl} \mbox{maximize} & \EMean^\mathsf{T}\mathbf{x} & &\\ \mbox{subject to} & \left(\gamma^2, \frac{1}{2}, \mathbf{G}^\mathsf{T}\mathbf{x}\right) & \in & \Q_\mathrm{r}^{N+2},\\ & \mathbf{1}^\mathsf{T}\mathbf{x} & = & 1,\\ & \mathbf{x} & \geq & 0.\\ \end{array} $$
Here we define this model in MOSEK Fusion.
# Define function solving the optimization model
def Markowitz(N, m, G, gamma2):
with Model("markowitz") as M:
# Settings
M.setLogHandler(sys.stdout)
# Decision variable (fraction of holdings in each security)
# The variable x is restricted to be positive, which imposes the constraint of no short-selling.
x = M.variable("x", N, Domain.greaterThan(0.0))
# Budget constraint
M.constraint('budget', Expr.sum(x), Domain.equalsTo(1))
# Objective
M.objective('obj', ObjectiveSense.Maximize, Expr.dot(m, x))
# Imposes a bound on the risk
M.constraint('risk', Expr.vstack(gamma2, 0.5, Expr.mul(G.transpose(), x)), Domain.inRotatedQCone())
# Solve optimization
M.solve()
# Check if the solution is an optimal point
solsta = M.getPrimalSolutionStatus()
if (solsta != SolutionStatus.Optimal):
# See https://docs.mosek.com/latest/pythonfusion/accessing-solution.html about handling solution statuses.
raise Exception("Unexpected solution status!")
returns = M.primalObjValue()
portfolio = x.level()
return returns, portfolio
The problem parameters are the number of securities $N$ and the risk limit $\gamma^2$.
N = mu.shape[0] # Number of securities
gamma2 = 0.05 # Risk limit (variance)
Here we factorize $\ECov$ because the model is defined in conic form, and it expects a matrix $G$ such that $\ECov = GG^\mathsf{T}$.
G = np.linalg.cholesky(Sigma) # Cholesky factor of S to use in conic risk constraint
Next we call the function that defines the Fusion model and runs the optimization.
# Run optimization
f, x = Markowitz(N, mu, G, gamma2)
print("========================\n")
print("RESULTS:")
print(f"Optimal expected portfolio return: {f*100:.4f}%")
print(f"Optimal portfolio weights: {x}")
print(f"Sum of weights: {np.sum(x)}")
Problem Name : markowitz Objective sense : maximize Type : CONIC (conic optimization problem) Constraints : 1 Affine conic cons. : 1 Disjunctive cons. : 0 Cones : 0 Scalar variables : 9 Matrix variables : 0 Integer variables : 0 Optimizer started. Presolve started. Eliminator started. Freed constraints in eliminator : 0 Eliminator terminated. Linear dependency checker started. Linear dependency checker terminated. Eliminator started. Freed constraints in eliminator : 0 Eliminator terminated. Eliminator - tries : 2 time : 0.00 Lin. dep. - tries : 1 time : 0.00 Lin. dep. - number : 0 Presolve terminated. Time: 0.00 Problem Name : markowitz Objective sense : maximize Type : CONIC (conic optimization problem) Constraints : 1 Affine conic cons. : 1 Disjunctive cons. : 0 Cones : 0 Scalar variables : 9 Matrix variables : 0 Integer variables : 0 Optimizer - threads : 20 Optimizer - solved problem : the primal Optimizer - Constraints : 8 Optimizer - Cones : 1 Optimizer - Scalar variables : 16 conic : 9 Optimizer - Semi-definite variables: 0 scalarized : 0 Factor - setup time : 0.00 dense det. time : 0.00 Factor - ML order time : 0.00 GP order time : 0.00 Factor - nonzeros before factor : 36 after factor : 36 Factor - dense dim. : 0 flops : 6.00e+02 ITE PFEAS DFEAS GFEAS PRSTATUS POBJ DOBJ MU TIME 0 1.0e+00 1.6e+00 1.2e+00 0.00e+00 0.000000000e+00 2.236067977e-01 1.0e+00 0.01 1 1.9e-01 3.1e-01 5.2e-02 7.17e-01 3.603783622e-01 5.403581465e-01 1.9e-01 0.02 2 7.2e-02 1.1e-01 1.1e-02 2.13e+00 3.359129710e-01 3.727555242e-01 7.2e-02 0.02 3 3.9e-02 6.1e-02 4.4e-03 1.55e+00 2.940729650e-01 3.097931685e-01 3.9e-02 0.03 4 9.7e-03 1.5e-02 5.2e-04 1.34e+00 2.834101970e-01 2.867604611e-01 9.7e-03 0.03 5 3.2e-03 5.1e-03 1.0e-04 1.09e+00 2.789097434e-01 2.799847756e-01 3.2e-03 0.03 6 5.2e-04 8.2e-04 6.7e-06 9.93e-01 2.770268510e-01 2.771990381e-01 5.2e-04 0.03 7 1.1e-04 1.8e-04 6.6e-07 9.95e-01 2.767639389e-01 2.768013245e-01 1.1e-04 0.03 8 1.9e-06 3.0e-06 1.4e-09 1.00e+00 2.767188047e-01 2.767194670e-01 1.9e-06 0.03 9 1.9e-08 3.0e-08 1.4e-12 1.00e+00 2.767173194e-01 2.767173259e-01 1.9e-08 0.03 Optimizer terminated. Time: 0.04 Interior-point solution summary Problem status : PRIMAL_AND_DUAL_FEASIBLE Solution status : OPTIMAL Primal. obj: 2.7671731936e-01 nrm: 1e+00 Viol. con: 8e-09 var: 0e+00 acc: 2e-09 Dual. obj: 2.7671732594e-01 nrm: 6e+00 Viol. con: 0e+00 var: 4e-09 acc: 0e+00 ======================== RESULTS: Optimal expected portfolio return: 27.6717% Optimal portfolio weights: [0. 0.09127 0.2691 0. 0.02529 0.32163 0.17653 0.11618] Sum of weights: 0.9999999915763336
expected_x = np.array([0., 0.09126, 0.26911, 0., 0.02531, 0.32162, 0.17652, 0.11618])
diff = np.sum(np.abs(expected_x - x))
assert diff < 1e-4, f"Resulting portfolio does not match expected one. Difference is {diff}"