Note

Download this `Jupyter notebook`

and all
`data`

(unzip next to the ipynb file!).
You will need a Gurobi license to run this notebook, please follow the
license instructions.

# Market Impact Costs#

The *standard mean-variance (Markowitz) portfolio selection model* determines an optimal investment portfolio that balances risk and expected return. In this notebook, we maximize the portfolio’s expected return while constraining the admissible variance (risk) to a given maximum level. Please refer to the annotated list of references for more background information on portfolio optimization.

In this notebook, we consider a variant of this basic model in which we rebalance an existing portfolio while: - accounting for *market impact costs*, - enforcing minimum transaction sizes, and - including a *risk-free asset*.

Transaction costs can be implicit or explicit. Explicit or direct transaction costs are observable, directly charged to the investor, and can have a fixed component and a variable one proportional to the transaction volume. Implicit or indirect costs, often called *market impact costs*, result from imperfect markets. For example, they may result from market or liquidity restrictions (e.g., bid-ask spreads) and depend on the order-book situation when an order is executed. They are not typically
itemized explicitly (sometimes called hidden costs), and it can be difficult for investors to recognize them.

Market impact costs occur because the transaction changes the stock’s market price. They are defined as the difference between the transaction price and the (unperturbed) market price that would have prevailed if the trade had not occurred. Market impact costs can be viewed as an additional fee for the immediate execution of large trades. For large transactions, they can be much larger than the direct transaction costs. They are particularly important for institutional investors, for whom they can represent a major proportion of the total transaction costs. Market impact costs are typically nonlinear and pose exacerbated numerical optimization challenges.

```
[2]:
```

```
import gurobipy as gp
import pandas as pd
import numpy as np
import scipy.linalg as la
import random
import matplotlib.pyplot as plt
```

## Input Data#

The following input data is used within the model:

\(S\): set of stocks

\(\mu\): vector of expected returns

\(\Sigma\): PSD variance-covariance matrix

\(\sigma_{ij}\) covariance between returns of assets \(i\) and \(j\)

\(\sigma_{ii}\) variance of return of asset \(i\)

```
[4]:
```

```
# Import some example data set
Sigma = pd.read_pickle("sigma.pkl")
mu = pd.read_pickle("mu.pkl")
```

## Formulation#

The model rebalances an existing portfolio by maximizing the expected portfolio return while ensuring that the variance of the portfolio return does not exceed a specified level; it also accounts for market impact costs while requiring a minimal transaction size, and the inclusion of a risk free asset. This results in a quadratically constraint mixed-integer optimization problem.

### Model Parameters#

\(\bar\sigma^2\): maximal admissible variance for the portfolio return

\(\mu_\text{rf}\): risk-free return

\(\ell>0\): lower bound on transaction size

\(x^0_i\): holdings of asset \(i\) in the initial portfolio. Usually, the starting portfolio is given by the previous trading period. In this notebook, we choose a simple starting portfolio by uniformly distributing capital among every 20th asset.

\(c^{\text{mic}}\): unit market impact cost

```
[5]:
```

```
# Values for the model parameters:
V = 3 # Maximal admissible variance (sigma^2)
mu_rf = 3.5 / 52 # Risk-free return rate
l = 0.001 # Minimal transaction size
# Initial proportion of investment, uniformly distributed among every 20th asset
x0 = np.zeros(mu.shape)
x0[::20] = 1.0
x0 /= x0.sum()
# Unit market impact cost coefficients (see below)
c_mic = 0.01 * np.ones(mu.shape)
```

### Decision Variables#

We need several sets of decision variables:

The proportions of capital invested in the rebalanced portfolio among the considered stocks. The corresponding vector of positions is denoted by \(x\) with its component \(x_i\) denoting the proportion of capital invested in stock \(i\).

The proportion of capital invested into the risk-free asset is denoted by \(x_\text{rf}\). No capital is allocated to the risk-free asset in the initial portfolio.

Transaction amount variables \(y\) whose components \(y_i\) represent the absolute value of the difference between the position in asset \(i\) in the initial portfolio and in the rebalanced portfolio.

The other sets of variables distinguish between buys and sells:

The proportions of each stock

*bought*and included in the rebalanced portfolio. The corresponding vector of positions is denoted by \(x^+\) with its component \(x^+_i\) denoting the proportion of capital representing the purchase of stock \(i\).The proportions of each stock

*sold*from the existing portfolio. The corresponding vector of sales is denoted by \(x^-\) with its component \(x^-_i\) denoting the proportion of capital representing the sale of stock \(i\).Binary variables \(b_i^+\) indicating whether or not asset \(i\) is

*bought*. If \(b_i^+\) is 0, asset \(i\) is not bought, that is \(x_i \leq x^0_i\). Otherwise, if \(b_i^+\) is 1, the investor buys some amount (at least \(\ell\)) of asset \(i\) and \(x_i \geq x^0_i + \ell\).Binary variables \(b_i^-\) indicating whether or not asset \(i\) is

*sold*. If \(b_i^-\) is 0, asset \(i\) is not sold, that is \(x_i \geq x^0_i\). Otherwise, if \(b_i^-\) is 1, the investor sells some amount (at least \(\ell\)) of asset \(i\) and \(x_i \leq x^0_i - \ell\).

### Variable Bounds#

Each position must be between 0 and 1; this prevents leverage and short-selling:

The same bounds apply to the buy and sell proportions and the transaction amounts:

Variables \(b_i^+, b_i^-\) are binary:

```
[6]:
```

```
%%capture
# Create an empty optimization model
m = gp.Model()
# Add variables: x[i] denotes the proportion invested in stock i
x = m.addMVar(len(mu), lb=0, ub=1, name="x")
# Add variable: xrf denotes the proportion of risk-free asset
xrf = m.addVar(lb=0, ub=1, name="x_rf")
# Add variables: y[i] denotes the proportion of stock i traded
# y = m.addMVar(len(mu), lb=0, ub=1-x0, name='y')
y = m.addMVar(len(mu), lb=0, ub=1, name="y")
# Add variables: x_plus[i] denotes the proportion of stock i bought
x_plus = m.addMVar(len(mu), lb=0, ub=1, name="x_plus")
# Add variables: x_minus[i] denotes the proportion of stock i sold
x_minus = m.addMVar(len(mu), lb=0, ub=1, name="x_minus")
# Add variables: b_plus[i]=1 if stock i is bought, and b_plus[i]=0 otherwise
b_plus = m.addMVar(len(mu), vtype=gp.GRB.BINARY, name="b_plus")
# Add variables: b_minus[i]=1 if stock i is sold, and b_minus[i]=0 otherwise
b_minus = m.addMVar(len(mu), vtype=gp.GRB.BINARY, name="b_minus")
```

### Constraints#

The final proportion of capital invested in a stock in the rebalanced portfolio is obtained by taking into account the buys and sells of this stock as well as the initial holdings in the existing portfolio:

\begin{equation*} x_i = x^0_i + x^+_i - x^-_i \; , \; i \in S \tag{1} \end{equation*}

The estimated risk must not exceed a prespecified maximal admissible level of variance \(\bar\sigma^2\): \begin{equation*} x^\top \Sigma x \leq \bar\sigma^2\tag{2} \end{equation*}

```
[7]:
```

```
%%capture
# Position rebalancing constraint; see formula (1) above
m.addConstr(x == x0 + x_plus - x_minus, name="Position_Balance")
# Upper bound on variance; see formula (2) above
m.addConstr(x @ Sigma.to_numpy() @ x <= V, name="Variance")
```

#### Transaction Amount Constraints#

The transaction amount variables represent the absolute value of the difference between the position in asset \(i\) in the existing portfolio and in the rebalanced portfolio, that is \(y_i = |x_i - x_i^0|\). They will be needed later for the market impact costs and are defined using the following constraints:

\begin{equation*} y_i = x^+_i + x^-_i \; , \; i \in S \tag{3} \end{equation*}

Note that in the optimal solution at most one of the two variables \(x^+_i\) and \(x^-_i\) should take a nonzero value for each \(i\).

To enforce this (and the minimal transaction size), we use the binary variables \(b^+,b^-\) and the following sets of discrete constraints, which will enforce:

\begin{equation*} x_i^+ \geq \ell\ \text{ if } b_i^+=1 \ \text{ and } x_i^+ = 0 \text{ otherwise} \\ \end{equation*} \begin{equation*} x_i^- \geq \ell\ \text{ if } b_i^-=1 \ \text{ and } x_i^- = 0 \text{ otherwise} \end{equation*}

Ensure that \(x_i^+ = 0\) if \(b_i^+ = 0\):

\begin{equation*} x_i^+ \leq b_i^+ \; , \; i \in S\tag{4} \end{equation*}

Since \(x_i^+\) has an upper bound of 1, the above constraint is non-restrictive when \(b_i^+ = 1\).

Ensure a minimal transaction size of \(\ell\) if asset \(i\) is traded:

\begin{equation*} x_i^+ \geq \ell b_i^+ \; , \; i \in S\tag{5} \end{equation*}

Hence \(b_i^+ = 1\) implies \(x_i^+ \geq \ell\). If \(b_i^+ = 0\), this constraint is non-restrictive since \(x_i^+\) has a lower bound of 0.

Analogously, we define similar constraints for \(x_i^-\) and \(b_i^-\):

\begin{align*} x_i^- & \leq b_i^- \; , \; i \in S\\ x_i^- & \geq \ell b_i^- \; , \; i \in S \end{align*}

With these, we can define a constraint to ensure that no asset is bought and sold at the same time:

\begin{equation*} b^+_i + b^-_i \leq 1 \; , \; i \in S \tag{6} \end{equation*}

```
[8]:
```

```
%%capture
# Absolute value of trades; see formula (3) above
m.addConstr(y == x_plus + x_minus, name="Transaction_Amount")
# Force x_plus, x_minus to 0 if not traded; see formula (4) above
m.addConstr(x_plus <= b_plus, name="Indicator_Buy")
m.addConstr(x_minus <= b_minus, name="Indicator_Sell")
# Minimal buy/sell; see formula (5) above
m.addConstr(x_plus >= l * b_plus, name="Minimal_Buy")
m.addConstr(x_minus >= l * b_minus, name="Minimal_Sell")
# Constraint preventing the simultaneous buy and sell of the same security; see formula (6) above
m.addConstr(b_plus + b_minus <= 1, name="Mutual_Exclusivity")
```

#### Market Impact Costs#

The definition of the *market impact costs* requires the introduction of a parameter \(c^\text{mic}_i\), which must be estimated ex-ante. For simplicity we set \(c^{\text{mic}}_i = 0.01\) for each \(i\) here. In reality, this value depends on the daily trade volume and other market data; see [1] for a discussion of the practical estimation of these constants.

The quantity \(c^{\text{mic}}_i y_i^{0.5}\) is an estimate of the unit market impact cost for asset \(i\), and \(\sum_{i \in S}c^{\text{mic}}_i y_{i}^{0.5}y_i\) is the total market impact cost.

Without any costs, we would have the budget constraint \(x_\text{rf} + \sum x_i = 1\) to ensure that all investments sum up to one. Since market impact cost is paid from the invested capital, we add this term to the budget constraint as follows:

\begin{equation} \underbrace{\underbrace{x_\text{rf}}_\text{risk-free allocation} + \underbrace{\sum_{i \in S}x_i}_\text{risky allocation}}_\text{investments} + \underbrace{\sum_{i \in S}(c^{\text{mic}}_i y_{i}^{0.5}) {y_i}}_\text{market impact costs} = 1 \end{equation}

This base formulation of the market impact cost contains a nonlinear term. Next, we explain how this can be modeled by quadratic constraints.

#### Reformulation with Quadratic Constraints#

Recall that the market impact cost for asset \(i\) is

\begin{equation*} (c^{\text{mic}}_i y_i^{0.5}) y_i = c^\text{mic}y^{1.5}\; , \; i \in S . \end{equation*}

From a modeling perspective, the difficult part is the term \(y_i^{1.5}\). But using the auxiliary variables \(w_i\) as variable upper bounds,

\begin{equation*} {y_i}^{1.5} \leq w_i \; , \; i \in S, \tag{7} \end{equation*}

we can obtain a convex reformulation of (7) using two quadratic constraints as follows:

\begin{align*} & y_i^2 \leq u_i w_i \; , \; & i \in S\tag{8}\\ & u_i^2 \leq y_i \; , \; & i \in S\tag{9}\\ & w_i, u_i \geq 0 \; , \; & i \in S. \end{align*}

Substituting \(u_i\) in (8) using (9), we obtain (7), showing the equivalence. The budget constraint hence becomes a linear equation:

\begin{equation*} x_\text{rf} + \sum_{i \in S}x_i + \sum_{i \in S} c^{\text{mic}}_i w_i = 1 \tag{10} \end{equation*}

Note that \(w_i\) is just an *upper bound* for \(y_i^{1.5}\) in this formulation. However, the two values should agree (up to numerical tolerances) in an optimal solution. This is ensured if there is a risk-free asset with a positive return. Otherwise, we could get \(w_i > y_i^{1.5}\) and assume higher costs than necessary, thereby reducing the variance in order to satisfy the variance constraint.

```
[9]:
```

```
# Nonnegative auxiliary variables
w = m.addMVar(len(mu))
u = m.addMVar(len(mu))
# Constraints (8) and (9) above
m.addConstr((y**2 <= u * w), name="Market_Impact1")
m.addConstr((u**2 <= y), name="Market_Impact2")
# Budget constraint including market impact costs; see formula (10) above
budget_constr = m.addConstr(xrf + x.sum() + c_mic @ w == 1, name="Budget_Constraint")
```

#### Other Exponents and Second-Order Cones#

As shown above, the market impact costs for asset \(i\) are given by \(c^{\text{mic}}_i y_i^{1.5}\), and we were able to incorporate these costs by means of quadratic constraints. The methodology behind this though is much more general: It can be generalized for any rational exponent \(\frac pq\) using second-order cone modeling tricks.

For example, for the exponent \(1.5=\frac32\) used above the explicit reformulation as rotated second-order cone constraints would be:

\begin{align*} & y_i^2 \leq 2 v_i w_i \; , \; & i \in S\\ & v_i^2 \leq 2 y_i q_i \; , \; & i \in S\\ & q_i = 0.125 \; , \; & i \in S\\ & w_i, v_i \geq 0 \; , \; & i \in S \end{align*}

The technical details for arbitrary rational exponents go beyond the scope of this notebook; see [2, Section 3.3] for details.

### Objective Function#

The objective is to maximize the expected portfolio return of the portfolio and consists of two parts: * the risk-free return and * the expected risky return:

```
[10]:
```

```
# define objective: maximize expected return
m.setObjective(xrf * mu_rf + mu.to_numpy() @ x, gp.GRB.MAXIMIZE)
```

We now solve the optimization problem:

```
[11]:
```

```
m.Params.MIPGap = 0.01
m.optimize()
```

```
Set parameter MIPGap to value 0.01
```

```
Gurobi Optimizer version 11.0.0 build v11.0.0rc2 (linux64 - "Ubuntu 22.04.4 LTS")
```

```
```

```
CPU model: Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz, instruction set [SSE2|AVX|AVX2|AVX512]
```

```
Thread count: 2 physical cores, 2 logical processors, using up to 2 threads
```

```
```

```
WLS license 2443533 - registered to Gurobi GmbH
```

```
Optimize a model with 3235 rows, 3697 columns and 8317 nonzeros
```

```
Model fingerprint: 0x2896c41a
```

```
Model has 925 quadratic constraints
```

```
Variable types: 2773 continuous, 924 integer (924 binary)
```

```
Coefficient statistics:
```

```
Matrix range [1e-03, 1e+00]
```

```
QMatrix range [3e-03, 1e+02]
```

```
QLMatrix range [1e+00, 1e+00]
```

```
Objective range [7e-02, 6e-01]
```

```
Bounds range [1e+00, 1e+00]
```

```
RHS range [4e-02, 1e+00]
```

```
QRHS range [3e+00, 3e+00]
```

```
Presolve removed 2214 rows and 1776 columns
```

```
Presolve time: 0.19s
```

```
Presolved: 1021 rows, 1921 columns, 3037 nonzeros
```

```
Presolved model has 925 quadratic constraint(s)
```

```
Variable types: 1435 continuous, 486 integer (486 binary)
```

```
```

```
Root relaxation: objective 5.920509e-01, 50 iterations, 0.00 seconds (0.00 work units)
```

```
```

```
Nodes | Current Node | Objective Bounds | Work
```

```
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
```

```
```

```
H 0 0 0.2382711 0.59205 148% - 0s
```

```
* 0 0 0 0.2382711 0.59205 148% - 0s
```

```
H 0 0 0.2862615 0.55248 93.0% - 0s
```

```
* 0 0 0 0.2862615 0.55248 93.0% - 0s
```

```
0 0 0.43186 0 - 0.28626 0.43186 50.9% - 0s
```

```
H 0 0 0.2862700 0.41259 44.1% - 0s
```

```
* 0 0 0 0.2862700 0.41259 44.1% - 0s
```

```
H 0 0 0.2894166 0.39032 34.9% - 0s
```

```
* 0 0 0 0.2894166 0.39032 34.9% - 0s
```

```
H 0 0 0.2990366 0.38664 29.3% - 0s
```

```
* 0 0 0 0.2990366 0.38664 29.3% - 0s
```

```
0 0 0.37424 0 - 0.29904 0.37424 25.1% - 0s
```

```
0 0 0.36737 0 - 0.29904 0.36737 22.9% - 0s
```

```
H 0 0 0.3011375 0.36538 21.3% - 0s
```

```
* 0 0 0 0.3011375 0.36538 21.3% - 0s
```

```
H 0 0 0.3023644 0.36460 20.6% - 0s
```

```
* 0 0 0 0.3023644 0.36460 20.6% - 0s
```

```
0 0 0.36388 0 - 0.30236 0.36388 20.3% - 0s
```

```
0 0 0.36358 0 - 0.30236 0.36358 20.2% - 0s
```

```
0 0 0.36205 0 - 0.30236 0.36205 19.7% - 0s
```

```
0 0 0.36108 0 - 0.30236 0.36108 19.4% - 0s
```

```
0 0 0.36079 0 - 0.30236 0.36079 19.3% - 0s
```

```
0 0 0.35768 0 - 0.30236 0.35768 18.3% - 0s
```

```
0 0 0.35639 0 - 0.30236 0.35639 17.9% - 0s
```

```
0 0 0.35425 0 - 0.30236 0.35425 17.2% - 0s
```

```
0 0 0.35345 0 - 0.30236 0.35345 16.9% - 0s
```

```
0 0 0.35322 0 - 0.30236 0.35322 16.8% - 0s
```

```
0 0 0.35187 0 - 0.30236 0.35187 16.4% - 0s
```

```
0 0 0.35101 0 - 0.30236 0.35101 16.1% - 0s
```

```
0 0 0.35088 0 - 0.30236 0.35088 16.0% - 0s
```

```
0 0 0.34956 0 - 0.30236 0.34956 15.6% - 0s
```

```
0 0 0.34845 0 - 0.30236 0.34845 15.2% - 0s
```

```
0 0 0.34814 0 - 0.30236 0.34814 15.1% - 0s
```

```
0 0 0.34785 0 - 0.30236 0.34785 15.0% - 0s
```

```
0 0 0.34675 0 - 0.30236 0.34675 14.7% - 0s
```

```
0 0 0.34414 0 - 0.30236 0.34414 13.8% - 0s
```

```
0 0 0.34373 0 - 0.30236 0.34373 13.7% - 0s
```

```
0 0 0.34330 0 - 0.30236 0.34330 13.5% - 0s
```

```
0 0 0.34321 0 - 0.30236 0.34321 13.5% - 0s
```

```
0 0 0.34226 0 - 0.30236 0.34226 13.2% - 0s
```

```
0 0 0.34189 0 - 0.30236 0.34189 13.1% - 0s
```

```
0 0 0.34136 0 - 0.30236 0.34136 12.9% - 0s
```

```
0 0 0.33996 0 - 0.30236 0.33996 12.4% - 0s
```

```
0 0 0.33964 0 - 0.30236 0.33964 12.3% - 0s
```

```
0 0 0.33918 0 - 0.30236 0.33918 12.2% - 0s
```

```
0 0 0.33903 0 - 0.30236 0.33903 12.1% - 0s
```

```
0 0 0.33887 0 - 0.30236 0.33887 12.1% - 0s
```

```
0 0 0.33791 0 - 0.30236 0.33791 11.8% - 0s
```

```
0 0 0.33737 0 - 0.30236 0.33737 11.6% - 0s
```

```
0 0 0.33712 0 - 0.30236 0.33712 11.5% - 0s
```

```
0 0 0.33656 0 1 0.30236 0.33656 11.3% - 0s
```

```
0 0 0.33656 0 1 0.30236 0.33656 11.3% - 3s
```

```
H 0 0 0.3134699 0.33656 7.36% - 3s
```

```
H 0 0 0.3136772 0.33656 7.29% - 4s
```

```
H 0 0 0.3161336 0.33656 6.46% - 4s
```

```
H 0 0 0.3164679 0.33656 6.35% - 5s
```

```
H 0 0 0.3167278 0.33656 6.26% - 5s
```

```
H 0 0 0.3170450 0.33656 6.15% - 5s
```

```
H 0 0 0.3177684 0.33656 5.91% - 5s
```

```
* 0 2 0 0.3177684 0.32702 2.91% - 5s
```

```
H 65 1 0.3178682 0.32305 1.63% 59.1 7s
```

```
* 65 1 2 0.3178682 0.32288 1.58% 60.2 7s
```

```
H 103 4 0.3179027 0.32208 1.31% 57.3 7s
```

```
* 103 4 5 0.3179027 0.32208 1.31% 57.9 7s
```

```
H 125 5 0.3179115 0.32197 1.28% 56.6 7s
```

```
* 125 5 3 0.3179115 0.32194 1.27% 56.8 7s
```

```
H 153 8 0.3180080 0.32177 1.18% 56.2 7s
```

```
* 153 8 5 0.3180080 0.32174 1.17% 56.7 7s
```

```
H 174 11 0.3180394 0.32163 1.13% 55.2 8s
```

```
* 174 11 4 0.3180394 0.32161 1.12% 55.6 8s
```

```
```

```
Explored 220 nodes (14889 simplex iterations) in 8.39 seconds (6.71 work units)
```

```
Thread count was 2 (of 2 available processors)
```

```
```

```
Solution count 10: 0.318039 0.318008 0.317911 ... 0.316134
```

```
```

```
Optimal solution found (tolerance 1.00e-02)
```

```
Best objective 3.180394161690e-01, best bound 3.212061344743e-01, gap 0.9957%
```

Display basic solution data; for clarity we’ve rounded all solution quantities to five digits.

```
[12]:
```

```
print(f"Expected return: {m.ObjVal:.6f}")
print(f"Variance: {x.X @ Sigma @ x.X:.6f}")
print(f"Solution time: {m.Runtime:.2f} seconds\n")
print(f"Market impact cost: {c_mic @ w.X:.6f}\n")
print(
f"Number of positions: before {np.count_nonzero(x0[x0>1e-5])}, after {np.count_nonzero(x.X[x.X>1e-5])}"
)
print(f"Risk-free allocation: before 0, after {xrf.X:.6f}")
print(
f"Number of trades: {sum(np.round(b_plus.X)) + sum(np.round(b_minus.X))} ({sum(np.round(b_plus.X))} buy(s), {sum(np.round(b_minus.X))} sell(s))\n"
)
# Print all assets with either a non-negligible position or transaction
df = pd.DataFrame(
index=mu.index,
data={
"position": x.X,
"transaction": x.X - x0,
"market impact cost": c_mic * y.X**1.5,
},
).round(6)
df[(df["position"] > 1e-5) | (abs(df["transaction"]) > 1e-5)].sort_values(
"position", ascending=False
)
```

```
Expected return: 0.318039
Variance: 2.999990
Solution time: 8.40 seconds
Market impact cost: 0.004572
Number of positions: before 24, after 23
Risk-free allocation: before 0, after 0.022550
Number of trades: 46.0 (23.0 buy(s), 23.0 sell(s))
```

```
[12]:
```

position | transaction | market impact cost | |
---|---|---|---|

LLY | 0.208309 | 0.208309 | 0.000951 |

PGR | 0.117209 | 0.117209 | 0.000401 |

KDP | 0.089336 | 0.089336 | 0.000267 |

NVDA | 0.062555 | 0.062555 | 0.000156 |

KR | 0.049349 | 0.049349 | 0.000110 |

TMUS | 0.048793 | 0.048793 | 0.000108 |

DPZ | 0.045042 | 0.045042 | 0.000096 |

TTWO | 0.042924 | 0.042924 | 0.000089 |

WM | 0.042677 | 0.001010 | 0.000000 |

NOC | 0.041804 | 0.041804 | 0.000085 |

AVGO | 0.038690 | 0.038690 | 0.000076 |

ORLY | 0.037806 | 0.037806 | 0.000074 |

ODFL | 0.031108 | 0.031108 | 0.000055 |

WST | 0.023893 | 0.023893 | 0.000037 |

MSFT | 0.022807 | 0.022807 | 0.000034 |

META | 0.012661 | 0.012661 | 0.000014 |

UNH | 0.011994 | 0.011994 | 0.000013 |

MNST | 0.011496 | 0.011496 | 0.000012 |

NFLX | 0.011452 | 0.011452 | 0.000012 |

AZO | 0.010386 | 0.010386 | 0.000011 |

DXCM | 0.008838 | 0.008838 | 0.000008 |

ENPH | 0.002170 | 0.002170 | 0.000001 |

MOH | 0.001572 | 0.001572 | 0.000001 |

CPRT | 0.000002 | -0.041665 | 0.000085 |

BR | 0.000002 | -0.041665 | 0.000085 |

SBUX | 0.000000 | -0.041667 | 0.000085 |

APTV | 0.000000 | -0.041667 | 0.000085 |

RF | 0.000000 | -0.041666 | 0.000085 |

NTRS | 0.000000 | -0.041667 | 0.000085 |

CHRW | 0.000000 | -0.041667 | 0.000085 |

NVR | 0.000000 | -0.041666 | 0.000085 |

PSX | 0.000000 | -0.041667 | 0.000085 |

GPC | 0.000000 | -0.041667 | 0.000085 |

GIS | 0.000000 | -0.041666 | 0.000085 |

LDOS | 0.000000 | -0.041666 | 0.000085 |

SLB | 0.000000 | -0.041667 | 0.000085 |

PARA | 0.000000 | -0.041667 | 0.000085 |

HD | 0.000000 | -0.041666 | 0.000085 |

HSIC | 0.000000 | -0.041667 | 0.000085 |

DIS | 0.000000 | -0.041667 | 0.000085 |

FANG | 0.000000 | -0.041667 | 0.000085 |

MET | 0.000000 | -0.041667 | 0.000085 |

BRO | 0.000000 | -0.041666 | 0.000085 |

PSA | 0.000000 | -0.041666 | 0.000085 |

JKHY | 0.000000 | -0.041666 | 0.000085 |

UNP | 0.000000 | -0.041667 | 0.000085 |

## Comparison with the unconstrained portfolio#

We can also optimize the portfolio without considering the market impact costs and compare the resulting portfolios.

```
[13]:
```

```
# remove budget constraint
m.remove(budget_constr)
# add new budget constraint without costs
m.addConstr(x.sum() == 1, name="Budget_Constraint")
m.params.OutputFlag = 0
m.optimize()
# retrieve and display solution data
positions_no_costs = pd.Series(name="Position", data=x.X, index=mu.index)
mask = (df["position"] > 1e-5) | (positions_no_costs > 1e-5)
df2 = pd.DataFrame(
index=df[mask].index,
data={
"with market impact costs": df["position"],
"without market impact costs": positions_no_costs,
},
).sort_values("with market impact costs", ascending=True)
axs = df2.plot.barh(color=["#0b1a3c", "#dd2113"])
axs.set_xlabel("Fraction of investment sum")
plt.title("Minimum Variance portfolios with and without market impact costs")
plt.show()
```

## Takeaways#

Market impact costs with the power \(\frac32\) can be modeled using quadratic constraints.

Minimum transaction sizes in combination with market impact costs make this a combinatorial problem.

## Literature#

[1] Grinold R.C., Kahn R.N. 2000. Active Portfolio Management. McGraw-Hill, New York, Second Edition.

[2] Ben-Tal A. and Nemirovski A. 2001. Lectures on Modern Convex Optimization. SIAM.