Portfolio optimization#

Portfolio allocation vector#

In this example we show how to do portfolio optimization using CVXPY. We begin with the basic definitions. In portfolio optimization we have some amount of money to invest in any of $$n$$ different assets. We choose what fraction $$w_i$$ of our money to invest in each asset $$i$$, $$i=1, \ldots, n$$.

We call $$w\in {\bf R}^n$$ the portfolio allocation vector. We of course have the constraint that $${\mathbf 1}^T w =1$$. The allocation $$w_i<0$$ means a short position in asset $$i$$, or that we borrow shares to sell now that we must replace later. The allocation $$w \geq 0$$ is a long only portfolio. The quantity

$\|w \|_1 = {\mathbf 1}^T w_+ + {\mathbf 1}^T w_-$

is known as leverage.

Asset returns#

We will only model investments held for one period. The initial prices are $$p_i > 0$$. The end of period prices are $$p_i^+ >0$$. The asset (fractional) returns are $$r_i = (p_i^+-p_i)/p_i$$. The porfolio (fractional) return is $$R = r^Tw$$.

A common model is that $$r$$ is a random variable with mean $${\bf E}r = \mu$$ and covariance $${\bf E{(r-\mu)(r-\mu)^T}} = \Sigma$$. It follows that $$R$$ is a random variable with $${\bf E}R = \mu^T w$$ and $${\bf var}(R) = w^T\Sigma w$$. $${\bf E}R$$ is the (mean) return of the portfolio. $${\bf var}(R)$$ is the risk of the portfolio. (Risk is also sometimes given as $${\bf std}(R) = \sqrt{{\bf var}(R)}$$.)

Portfolio optimization has two competing objectives: high return and low risk.

Classical (Markowitz) portfolio optimization#

Classical (Markowitz) portfolio optimization solves the optimization problem

$\begin{split} \begin{array}{ll} \mbox{maximize} & \mu^T w - \gamma w^T\Sigma w\\ \mbox{subject to} & {\bf 1}^T w = 1, \quad w \in {\cal W}, \end{array} \end{split}$

where $$w \in {\bf R}^n$$ is the optimization variable, $$\cal W$$ is a set of allowed portfolios (e.g., $${\cal W} = {\bf R}_+^n$$ for a long only portfolio), and $$\gamma >0$$ is the risk aversion parameter.

The objective $$\mu^Tw - \gamma w^T\Sigma w$$ is the risk-adjusted return. Varying $$\gamma$$ gives the optimal risk-return trade-off. We can get the same risk-return trade-off by fixing return and minimizing risk.

Example#

In the following code we compute and plot the optimal risk-return trade-off for $$10$$ assets, restricting ourselves to a long only portfolio.

# Generate data for long only portfolio optimization.
import numpy as np
import scipy.sparse as sp

np.random.seed(1)
n = 10
mu = np.abs(np.random.randn(n, 1))
Sigma = np.random.randn(n, n)
Sigma = Sigma.T.dot(Sigma)

# Long only portfolio optimization.
import cvxpy as cp

w = cp.Variable(n)
gamma = cp.Parameter(nonneg=True)
ret = mu.T @ w
prob = cp.Problem(cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, w >= 0])

# Compute trade-off curve.
SAMPLES = 100
risk_data = np.zeros(SAMPLES)
ret_data = np.zeros(SAMPLES)
gamma_vals = np.logspace(-2, 3, num=SAMPLES)
for i in range(SAMPLES):
gamma.value = gamma_vals[i]
prob.solve()
risk_data[i] = cp.sqrt(risk).value
ret_data[i] = ret.value

/tmp/ipykernel_1931/3491241395.py:10: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)
ret_data[i] = ret.value

# Plot long only trade-off curve.
import matplotlib.pyplot as plt

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

markers_on = [29, 40]
fig = plt.figure()
plt.plot(risk_data, ret_data, "g-")
for marker in markers_on:
plt.plot(risk_data[marker], ret_data[marker], "bs")
ax.annotate(
r"$\gamma = %.2f$" % gamma_vals[marker],
xy=(risk_data[marker] + 0.08, ret_data[marker] - 0.03),
)
for i in range(n):
plt.plot(cp.sqrt(Sigma[i, i]).value, mu[i], "ro")
plt.xlabel("Standard deviation")
plt.ylabel("Return")
plt.show()


We plot below the return distributions for the two risk aversion values marked on the trade-off curve. Notice that the probability of a loss is near 0 for the low risk value and far above 0 for the high risk value.

# Plot return distributions for two points on the trade-off curve.
import scipy.stats as spstats

plt.figure()
for midx, idx in enumerate(markers_on):
gamma.value = gamma_vals[idx]
prob.solve()
x = np.linspace(-2, 5, 1000)
plt.plot(
x,
spstats.norm.pdf(x, ret.value, risk.value),
label=r"$\gamma = %.2f$" % gamma.value,
)

plt.xlabel("Return")
plt.ylabel("Density")
plt.legend(loc="upper right")
plt.show()


Portfolio constraints#

There are many other possible portfolio constraints besides the long only constraint. With no constraint ($${\cal W} = {\bf R}^n$$), the optimization problem has a simple analytical solution. We will look in detail at a leverage limit, or the constraint that $$\|w \|_1 \leq L^\mathrm{max}$$.

Another interesting constraint is the market neutral constraint $$m^T \Sigma w =0$$, where $$m_i$$ is the capitalization of asset $$i$$. $$M = m^Tr$$ is the market return, and $$m^T \Sigma w = {\bf cov}(M,R)$$. The market neutral constraint ensures that the portfolio return is uncorrelated with the market return.

Example#

In the following code we compute and plot optimal risk-return trade-off curves for leverage limits of 1, 2, and 4. Notice that more leverage increases returns and allows greater risk.

# Portfolio optimization with leverage limit.
Lmax = cp.Parameter()
prob = cp.Problem(
cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax]
)

# Compute trade-off curve for each leverage limit.
L_vals = [1, 2, 4]
SAMPLES = 100
risk_data = np.zeros((len(L_vals), SAMPLES))
ret_data = np.zeros((len(L_vals), SAMPLES))
gamma_vals = np.logspace(-2, 3, num=SAMPLES)
w_vals = []
for k, L_val in enumerate(L_vals):
for i in range(SAMPLES):
Lmax.value = L_val
gamma.value = gamma_vals[i]
prob.solve(solver=cp.SCS)
risk_data[k, i] = cp.sqrt(risk).value
ret_data[k, i] = ret.value

/tmp/ipykernel_1931/231315309.py:14: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)
ret_data[k, i] = ret.value

# Plot trade-off curves for each leverage limit.
for idx, L_val in enumerate(L_vals):
plt.plot(risk_data[idx, :], ret_data[idx, :], label=r"$L^{\max}$ = %d" % L_val)
for w_val in w_vals:
w.value = w_val
plt.plot(cp.sqrt(risk).value, ret.value, "bs")
plt.xlabel("Standard deviation")
plt.ylabel("Return")
plt.legend(loc="lower right")
plt.show()


We next examine the points on each trade-off curve where $$w^T\Sigma w = 2$$. We plot the amount of each asset held in each portfolio as bar graphs. (Negative holdings indicate a short position.) Notice that some assets are held in a long position for the low leverage portfolio but in a short position in the higher leverage portfolios.

# Portfolio optimization with a leverage limit and a bound on risk.
prob = cp.Problem(cp.Maximize(ret), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax, risk <= 2])

# Compute solution for different leverage limits.
for k, L_val in enumerate(L_vals):
Lmax.value = L_val
prob.solve()
w_vals.append(w.value)

# Plot bar graph of holdings for different leverage limits.
colors = ["b", "g", "r"]
indices = np.argsort(mu.flatten())
for idx, L_val in enumerate(L_vals):
plt.bar(
np.arange(1, n + 1) + 0.25 * idx - 0.375,
w_vals[idx][indices],
color=colors[idx],
label=r"$L^{\max}$ = %d" % L_val,
width=0.25,
)
plt.ylabel(r"$w_i$", fontsize=16)
plt.xlabel(r"$i$", fontsize=16)
plt.xlim([1 - 0.375, 10 + 0.375])
plt.xticks(np.arange(1, n + 1))
plt.show()


Variations#

There are many more variations of classical portfolio optimization. We might require that $$\mu^T w \geq R^\mathrm{min}$$ and minimize $$w^T \Sigma w$$ or $$\|\Sigma ^{1/2} w\|_2$$. We could include the (broker) cost of short positions as the penalty $$s^T (w)_-$$ for some $$s \geq 0$$. We could include transaction costs (from a previous portfolio $$w^\mathrm{prev}$$) as the penalty

$\kappa ^T |w-w^\mathrm{prev}|^\eta, \quad \kappa \geq 0.$

Common values of $$\eta$$ are $$\eta =1, ~ 3/2, ~2$$.

Factor covariance model#

A particularly common and useful variation is to model the covariance matrix $$\Sigma$$ as a factor model

$\Sigma = F \tilde \Sigma F^T + D,$

where $$F \in {\bf R}^{n \times k}$$, $$k \ll n$$ is the factor loading matrix. $$k$$ is the number of factors (or sectors) (typically 10s). $$F_{ij}$$ is the loading of asset $$i$$ to factor $$j$$. $$D$$ is a diagonal matrix; $$D_{ii}>0$$ is the idiosyncratic risk. $$\tilde \Sigma > 0$$ is the factor covariance matrix.

$$F^Tw \in {\bf R}^k$$ gives the portfolio factor exposures. A portfolio is factor $$j$$ neutral if $$(F^Tw)_j=0$$.

Portfolio optimization with factor covariance model#

Using the factor covariance model, we frame the portfolio optimization problem as

$\begin{split} \begin{array}{ll} \mbox{maximize} & \mu^T w - \gamma \left(f^T \tilde \Sigma f + w^TDw \right) \\ \mbox{subject to} & {\bf 1}^T w = 1, \quad f=F^Tw\\ & w \in {\cal W}, \quad f \in {\cal F}, \end{array} \end{split}$

where the variables are the allocations $$w \in {\bf R}^n$$ and factor exposures $$f\in {\bf R}^k$$ and $$\cal F$$ gives the factor exposure constraints.

Using the factor covariance model in the optimization problem has a computational advantage. The solve time is $$O(nk^2)$$ versus $$O(n^3)$$ for the standard problem.

Example#

In the following code we generate and solve a portfolio optimization problem with 30 factors and 3000 assets. We set the leverage limit $$=2$$ and $$\gamma=0.1$$.

We solve the problem both with the covariance given as a single matrix and as a factor model.

# Generate data for factor model.
n = 600
m = 30
np.random.seed(1)
mu = np.abs(np.random.randn(n, 1))
Sigma_tilde = np.random.randn(m, m)
Sigma_tilde = Sigma_tilde.T.dot(Sigma_tilde)
D = sp.diags(np.random.uniform(0, 0.9, size=n))
F = np.random.randn(n, m)

# Factor model portfolio optimization.
w = cp.Variable(n)
f = cp.Variable(m)
gamma = cp.Parameter(nonneg=True)
Lmax = cp.Parameter()
ret = mu.T @ w
risk = cp.quad_form(f, Sigma_tilde) + cp.sum_squares(np.sqrt(D) @ w)
prob_factor = cp.Problem(
cp.Maximize(ret - gamma * risk),
[cp.sum(w) == 1, f == F.T @ w, cp.norm(w, 1) <= Lmax],
)

# Solve the factor model problem.
Lmax.value = 2
gamma.value = 0.1
prob_factor.solve(verbose=True)

===============================================================================
CVXPY
v1.3.2
===============================================================================
(CVXPY) Jul 07 11:12:55 PM: Your problem has 630 variables, 3 constraints, and 2 parameters.

(CVXPY) Jul 07 11:12:55 PM: It is compliant with the following grammars: DCP, DQCP

(CVXPY) Jul 07 11:12:55 PM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.

-------------------------------------------------------------------------------
Compilation
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:12:55 PM: Compiling problem (target solver=OSQP).

(CVXPY) Jul 07 11:12:55 PM: Reduction chain: FlipObjective -> CvxAttr2Constr -> Qp2SymbolicQp -> QpMatrixStuffing -> OSQP

(CVXPY) Jul 07 11:12:55 PM: Applying reduction FlipObjective

(CVXPY) Jul 07 11:12:55 PM: Applying reduction CvxAttr2Constr

(CVXPY) Jul 07 11:12:55 PM: Applying reduction Qp2SymbolicQp

(CVXPY) Jul 07 11:12:55 PM: Applying reduction QpMatrixStuffing

(CVXPY) Jul 07 11:12:55 PM: Applying reduction OSQP

(CVXPY) Jul 07 11:12:55 PM: Finished problem compilation (took 2.346e-02 seconds).

(CVXPY) Jul 07 11:12:55 PM: (Subsequent compilations of this problem, using the same arguments, should take less time.)

-------------------------------------------------------------------------------
Numerical solver
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:12:55 PM: Invoking solver OSQP  to obtain a solution.

-----------------------------------------------------------------
OSQP v0.6.3  -  Operator Splitting QP Solver
(c) Bartolomeo Stellato,  Goran Banjac
University of Oxford  -  Stanford University 2021
-----------------------------------------------------------------
problem:  variables n = 1830, constraints m = 1832
nnz(P) + nnz(A) = 23895
settings: linear system solver = qdldl,
eps_abs = 1.0e-05, eps_rel = 1.0e-05,
eps_prim_inf = 1.0e-04, eps_dual_inf = 1.0e-04,
sigma = 1.00e-06, alpha = 1.60, max_iter = 10000
check_termination: on (interval 25),
scaling: on, scaled_termination: off
warm start: on, polish: on, time_limit: off

iter   objective    pri res    dua res    rho        time
1  -4.3898e+02   7.14e+00   3.28e+02   1.00e-01   7.11e-03s
200  -4.0679e+00   2.07e-03   7.49e-04   1.00e-01   2.27e-02s
400  -4.0680e+00   4.21e-04   6.77e-04   5.05e-01   3.90e-02s
600  -4.0399e+00   1.04e-04   1.64e-04   5.05e-01   5.46e-02s
800  -4.0344e+00   4.08e-05   1.19e-04   5.05e-01   7.02e-02s
875  -4.0351e+00   2.76e-05   2.62e-05   5.05e-01   7.61e-02s

status:               solved
solution polish:      unsuccessful
number of iterations: 875
optimal objective:    -4.0351
run time:             8.03e-02s
optimal rho estimate: 6.34e-01

-------------------------------------------------------------------------------
Summary
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:12:55 PM: Problem status: optimal

(CVXPY) Jul 07 11:12:55 PM: Optimal value: 4.035e+00

(CVXPY) Jul 07 11:12:55 PM: Compilation took 2.346e-02 seconds

(CVXPY) Jul 07 11:12:55 PM: Solver (including time spent in interface) took 8.182e-02 seconds

4.035127190680671

from cvxpy.atoms.affine.wraps import psd_wrap

# Standard portfolio optimization with data from factor model.
risk = cp.quad_form(w, psd_wrap(F.dot(Sigma_tilde).dot(F.T) + D))
prob = cp.Problem(
cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax]
)

prob.solve(verbose=True, max_iter=100_000)

===============================================================================
CVXPY
v1.3.2
===============================================================================
(CVXPY) Jul 07 11:12:55 PM: Your problem has 600 variables, 2 constraints, and 2 parameters.

(CVXPY) Jul 07 11:12:55 PM: It is compliant with the following grammars: DCP, DQCP

(CVXPY) Jul 07 11:12:55 PM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.

-------------------------------------------------------------------------------
Compilation
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:12:55 PM: Compiling problem (target solver=OSQP).

(CVXPY) Jul 07 11:12:55 PM: Reduction chain: FlipObjective -> CvxAttr2Constr -> Qp2SymbolicQp -> QpMatrixStuffing -> OSQP

(CVXPY) Jul 07 11:12:55 PM: Applying reduction FlipObjective

(CVXPY) Jul 07 11:12:55 PM: Applying reduction CvxAttr2Constr

(CVXPY) Jul 07 11:12:55 PM: Applying reduction Qp2SymbolicQp

(CVXPY) Jul 07 11:12:55 PM: Applying reduction QpMatrixStuffing

(CVXPY) Jul 07 11:12:55 PM: Applying reduction OSQP

(CVXPY) Jul 07 11:12:55 PM: Finished problem compilation (took 9.934e-02 seconds).

(CVXPY) Jul 07 11:12:55 PM: (Subsequent compilations of this problem, using the same arguments, should take less time.)

-------------------------------------------------------------------------------
Numerical solver
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:12:55 PM: Invoking solver OSQP  to obtain a solution.

-----------------------------------------------------------------
OSQP v0.6.3  -  Operator Splitting QP Solver
(c) Bartolomeo Stellato,  Goran Banjac
University of Oxford  -  Stanford University 2021
-----------------------------------------------------------------
problem:  variables n = 1200, constraints m = 1202
nnz(P) + nnz(A) = 183900
settings: linear system solver = qdldl,
eps_abs = 1.0e-05, eps_rel = 1.0e-05,
eps_prim_inf = 1.0e-04, eps_dual_inf = 1.0e-04,
sigma = 1.00e-06, alpha = 1.60, max_iter = 100000
check_termination: on (interval 25),
scaling: on, scaled_termination: off
warm start: on, polish: on, time_limit: off

iter   objective    pri res    dua res    rho        time
1  -1.8064e+03   7.86e+01   4.95e+03   1.00e-01   6.87e-02s

 200  -2.9381e+01   7.60e-02   2.60e-04   1.00e-01   1.69e-01s

 400  -1.3664e+01   3.22e-02   7.62e-05   1.00e-01   2.69e-01s

 600  -8.4293e+00   1.81e-02   3.21e-05   1.00e-01   3.70e-01s

 800  -6.2482e+00   1.09e-02   1.53e-05   1.00e-01   4.70e-01s

1000  -5.1966e+00   7.80e-03   7.36e-06   1.00e-01   5.71e-01s

1200  -4.6819e+00   6.08e-03   3.70e-06   1.00e-01   6.72e-01s

1400  -4.4246e+00   5.08e-03   1.96e-06   1.00e-01   7.72e-01s

1600  -4.2907e+00   4.45e-03   1.11e-06   1.00e-01   8.73e-01s

1800  -4.2168e+00   4.18e-03   6.81e-07   1.00e-01   9.74e-01s

2000  -4.1406e+00   3.80e-03   2.16e-05   5.10e-01   1.11e+00s

2200  -4.1575e+00   2.36e-03   1.11e-05   5.10e-01   1.21e+00s

2400  -4.1021e+00   2.01e-03   1.66e-05   5.10e-01   1.31e+00s

2600  -4.1081e+00   1.75e-03   2.84e-06   5.10e-01   1.41e+00s
2800  -4.1103e+00   1.73e-03   6.02e-07   5.10e-01   1.51e+00s

3000  -4.1054e+00   1.58e-03   7.73e-06   5.10e-01   1.61e+00s

3200  -4.1026e+00   1.45e-03   4.15e-06   5.10e-01   1.71e+00s

3400  -4.1038e+00   1.25e-03   1.93e-05   5.10e-01   1.81e+00s

3600  -4.0936e+00   9.19e-04   5.72e-06   5.10e-01   1.91e+00s

3800  -4.0887e+00   8.78e-04   3.79e-06   5.10e-01   2.01e+00s
4000  -4.0848e+00   8.77e-04   2.82e-06   5.10e-01   2.12e+00s

4200  -4.0863e+00   8.16e-04   2.36e-06   5.10e-01   2.22e+00s

4400  -4.0854e+00   7.30e-04   2.40e-06   5.10e-01   2.32e+00s
4600  -4.0858e+00   7.02e-04   9.78e-07   5.10e-01   2.42e+00s

4800  -4.0860e+00   6.88e-04   5.74e-07   5.10e-01   2.52e+00s

5000  -4.0857e+00   6.76e-04   4.10e-07   5.10e-01   2.62e+00s
5200  -4.0852e+00   6.66e-04   3.36e-07   5.10e-01   2.72e+00s

5400  -4.0848e+00   6.57e-04   2.77e-07   5.10e-01   2.82e+00s

5600  -4.0875e+00   5.79e-04   1.34e-06   5.10e-01   2.92e+00s

5800  -4.0823e+00   5.60e-04   7.75e-07   5.10e-01   3.02e+00s
6000  -4.0792e+00   5.43e-04   5.54e-07   5.10e-01   3.12e+00s

6200  -4.0770e+00   5.27e-04   4.39e-07   5.10e-01   3.22e+00s

6400  -4.0752e+00   5.13e-04   3.83e-07   5.10e-01   3.32e+00s
6600  -4.0740e+00   5.03e-04   3.87e-07   5.10e-01   3.42e+00s

6800  -4.0731e+00   4.92e-04   3.09e-07   5.10e-01   3.52e+00s

7000  -4.0722e+00   4.82e-04   2.81e-07   5.10e-01   3.62e+00s
7200  -4.0714e+00   4.76e-04   2.47e-07   5.10e-01   3.72e+00s

7400  -4.0707e+00   4.71e-04   2.16e-07   5.10e-01   3.82e+00s

7600  -4.0701e+00   4.66e-04   1.89e-07   5.10e-01   3.92e+00s

7800  -4.0696e+00   4.61e-04   1.67e-07   5.10e-01   4.02e+00s
8000  -4.0692e+00   4.56e-04   1.47e-07   5.10e-01   4.12e+00s

8200  -4.0687e+00   4.52e-04   1.31e-07   5.10e-01   4.23e+00s

8400  -4.0683e+00   4.48e-04   1.18e-07   5.10e-01   4.33e+00s
8600  -4.0680e+00   4.43e-04   1.07e-07   5.10e-01   4.43e+00s

8800  -4.0676e+00   4.39e-04   9.71e-08   5.10e-01   4.53e+00s

9000  -4.0673e+00   4.35e-04   8.91e-08   5.10e-01   4.63e+00s
9200  -4.0670e+00   4.31e-04   8.24e-08   5.10e-01   4.73e+00s

9400  -4.0667e+00   4.27e-04   7.68e-08   5.10e-01   4.83e+00s

9600  -4.0664e+00   4.23e-04   7.20e-08   5.10e-01   4.93e+00s

9800  -4.0662e+00   4.20e-04   6.80e-08   5.10e-01   5.03e+00s

10000  -4.0659e+00   4.16e-04   6.46e-08   5.10e-01   5.13e+00s

10200  -4.0562e+00   3.30e-04   1.03e-06   3.37e-01   5.30e+00s

10400  -4.0537e+00   2.88e-04   7.68e-07   3.37e-01   5.40e+00s
10600  -4.0519e+00   2.54e-04   6.13e-07   3.37e-01   5.50e+00s

10800  -4.0503e+00   2.25e-04   4.96e-07   3.37e-01   5.60e+00s
11000  -4.0489e+00   2.01e-04   4.06e-07   3.37e-01   5.70e+00s

11200  -4.0477e+00   1.81e-04   3.35e-07   3.37e-01   5.80e+00s
11400  -4.0466e+00   1.66e-04   2.79e-07   3.37e-01   5.90e+00s

11600  -4.0457e+00   1.59e-04   2.33e-07   3.37e-01   6.00e+00s
11800  -4.0448e+00   1.54e-04   1.95e-07   3.37e-01   6.10e+00s

12000  -4.0441e+00   1.49e-04   1.65e-07   3.37e-01   6.20e+00s
12200  -4.0435e+00   1.45e-04   1.41e-07   3.37e-01   6.30e+00s

12400  -4.0429e+00   1.42e-04   1.21e-07   3.37e-01   6.40e+00s
12600  -4.0424e+00   1.38e-04   1.04e-07   3.37e-01   6.50e+00s

12800  -4.0419e+00   1.36e-04   8.98e-08   3.37e-01   6.60e+00s
13000  -4.0415e+00   1.33e-04   7.80e-08   3.37e-01   6.70e+00s

13200  -4.0412e+00   1.31e-04   6.80e-08   3.37e-01   6.81e+00s
13400  -4.0408e+00   1.29e-04   5.95e-08   3.37e-01   6.91e+00s

13600  -4.0406e+00   1.27e-04   5.22e-08   3.37e-01   7.01e+00s
13800  -4.0403e+00   1.25e-04   4.60e-08   3.37e-01   7.11e+00s

14000  -4.0401e+00   1.24e-04   4.07e-08   3.37e-01   7.21e+00s
14200  -4.0398e+00   1.22e-04   3.62e-08   3.37e-01   7.31e+00s

14400  -4.0397e+00   1.21e-04   3.23e-08   3.37e-01   7.41e+00s
14600  -4.0395e+00   1.20e-04   2.89e-08   3.37e-01   7.51e+00s

14800  -4.0393e+00   1.18e-04   2.60e-08   3.37e-01   7.61e+00s
15000  -4.0392e+00   1.17e-04   2.35e-08   3.37e-01   7.71e+00s

15200  -4.0391e+00   1.16e-04   2.13e-08   3.37e-01   7.81e+00s
15400  -4.0389e+00   1.15e-04   1.94e-08   3.37e-01   7.91e+00s

15600  -4.0388e+00   1.14e-04   1.77e-08   3.37e-01   8.01e+00s
15800  -4.0384e+00   1.09e-04   3.04e-07   1.69e+00   8.15e+00s

16000  -4.0381e+00   1.05e-04   2.28e-07   1.69e+00   8.25e+00s
16200  -4.0379e+00   1.01e-04   1.86e-07   1.69e+00   8.35e+00s

16400  -4.0377e+00   9.71e-05   1.61e-07   1.69e+00   8.45e+00s
16600  -4.0376e+00   9.34e-05   1.45e-07   1.69e+00   8.55e+00s

16800  -4.0364e+00   7.54e-05   1.64e-06   1.69e+00   8.65e+00s
17000  -4.0358e+00   6.56e-05   1.00e-06   1.69e+00   8.75e+00s

17200  -4.0356e+00   6.02e-05   7.54e-07   1.69e+00   8.85e+00s
17400  -4.0354e+00   5.53e-05   5.92e-07   1.69e+00   8.95e+00s

17600  -4.0347e+00   4.84e-05   4.49e-08   2.33e-01   9.08e+00s
17800  -4.0346e+00   4.68e-05   3.30e-08   2.33e-01   9.18e+00s

18000  -4.0345e+00   4.55e-05   2.52e-08   2.33e-01   9.29e+00s
18200  -4.0344e+00   4.45e-05   2.13e-08   2.33e-01   9.39e+00s

18400  -4.0344e+00   4.35e-05   1.83e-08   2.33e-01   9.49e+00s
18600  -4.0344e+00   4.27e-05   1.59e-08   2.33e-01   9.59e+00s

18800  -4.0344e+00   4.19e-05   1.39e-08   2.33e-01   9.69e+00s
19000  -4.0345e+00   4.11e-05   1.23e-08   2.33e-01   9.79e+00s

19200  -4.0345e+00   4.04e-05   1.13e-08   2.33e-01   9.89e+00s
19400  -4.0345e+00   3.97e-05   1.04e-08   2.33e-01   9.99e+00s

19600  -4.0345e+00   3.91e-05   9.65e-09   2.33e-01   1.01e+01s
19800  -4.0346e+00   3.84e-05   8.98e-09   2.33e-01   1.02e+01s

20000  -4.0346e+00   3.78e-05   8.37e-09   2.33e-01   1.03e+01s
20200  -4.0346e+00   3.73e-05   7.83e-09   2.33e-01   1.04e+01s

20400  -4.0346e+00   3.67e-05   7.34e-09   2.33e-01   1.05e+01s
20600  -4.0346e+00   3.61e-05   6.90e-09   2.33e-01   1.06e+01s

20800  -4.0347e+00   3.56e-05   6.50e-09   2.33e-01   1.07e+01s
21000  -4.0347e+00   3.51e-05   6.13e-09   2.33e-01   1.08e+01s

21200  -4.0347e+00   3.46e-05   5.80e-09   2.33e-01   1.09e+01s
21400  -4.0347e+00   3.41e-05   5.49e-09   2.33e-01   1.10e+01s

21600  -4.0347e+00   3.36e-05   5.21e-09   2.33e-01   1.11e+01s
21800  -4.0348e+00   3.15e-05   1.04e-07   1.17e+00   1.12e+01s

21975  -4.0348e+00   2.98e-05   9.55e-08   1.17e+00   1.13e+01s

status:               solved
solution polish:      unsuccessful
number of iterations: 21975
optimal objective:    -4.0348
run time:             1.14e+01s
optimal rho estimate: 1.29e+00

-------------------------------------------------------------------------------
Summary
-------------------------------------------------------------------------------
(CVXPY) Jul 07 11:13:07 PM: Problem status: optimal

(CVXPY) Jul 07 11:13:07 PM: Optimal value: 4.035e+00

(CVXPY) Jul 07 11:13:07 PM: Compilation took 9.934e-02 seconds

(CVXPY) Jul 07 11:13:07 PM: Solver (including time spent in interface) took 1.137e+01 seconds

4.034822681213087


We expect the factor model to be at least 20 times faster than the standard model as $$n^2 = 20 n m$$ where $$n$$ is the number of assets and $$m$$ is the number of factors. This estimate does not yet reflect a smaller number of iterations required for the factor model.

print("Factor model solve time = {}".format(prob_factor.solver_stats.solve_time))
print("Single model solve time = {}".format(prob.solver_stats.solve_time))

Factor model solve time = 0.08027323900000001
Single model solve time = 11.360008881999999