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
risk = cp.quad_form(w, Sigma)
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()
ax = fig.add_subplot(111)
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()
../../../_images/a4be253fffa50a239680917b7b8b84ab60ec6d0c2c920d3cd1abee0715fe021b.svg

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()
../../../_images/602c162d9d37d5cae179ec04cd6c6f8c41c375b97caebf34ad242a71db7bc6c0.svg

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()
../../../_images/dfb0524a98a1e3ffdb359c6c39292baccd9d8ed672bdfce9086efd81dd56de36.svg

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()
../../../_images/656cee878d9599ddf4a190f11f3b0177a7963a1b11ddf017360679d614a1575f.svg

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,
          rho = 1.00e-01 (adaptive),
          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,
          rho = 1.00e-01 (adaptive),
          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