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.

# Leverage by Borrowing Cash#

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.

To this basic model, we add *leverage*. Leverage means borrowing capital from a third party to buy more assets (and paying interest on the borrowed capital). This magnifies both the potential upside and downside.

```
[2]:
```

```
import gurobipy as gp
import pandas as pd
import numpy as np
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#

Mathematically, this results in a convex quadratically constrained optimization problem.

### Model Parameters#

The following parameters are used within the model:

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

\(c_\text{rf}\): interest on the risk-free asset. For simplicity, we assume the same interest rate for lending and borrowing.

\(\ell_\text{rf}\): maximal short on risk-free asset

\(u_\text{rf}\): maximal investment in risk-free asset

```
[5]:
```

```
# Values for the model parameters:
V = 4.0 # Maximal admissible variance (sigma^2)
c_rf = 2 / 52 # interest rate on risk-free asset
l_rf = -0.3 # maximal borrowing of risk-free asset
u_rf = 1 # maximal investment in risk-free asset
```

### Decision Variables#

We require two types of decision variables:

The proportions of capital invested 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 \(x_\text{rf}\) invested in the risk-free asset. This may be positive or negative. If positive, we gain a risk-free return; if negative, we pay interest on the borrowed amount.

### Variable Bounds#

Each position must be nonnegative:

The risk-free position must be within its bounds:

Setting the upper bound \(u_\text{rf}=1\) means the portfolio is allowed to be fully invested in the risk-free asset.

```
[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), name="x")
# Risk-free allocation
x_rf = m.addVar(lb=l_rf, ub=u_rf, name="x_rf")
```

### Constraints#

The budget constraint ensures that all capital (both initial and borrowed) is invested:

The estimated risk must not exceed a prespecified maximal admissible level of variance \(\bar\sigma^2\):

```
[7]:
```

```
%%capture
# Budget constraint: all investments sum up to 1
m.addConstr(x.sum() + x_rf == 1, name="Budget_Constraint")
# Upper bound on variance
risk_constr = m.addConstr(x @ Sigma.to_numpy() @ x <= V, name="Variance")
```

### Objective Function#

The objective is to maximize the expected return of the portfolio. We need to account for risk-free returns and costs for borrowing cash: \begin{equation*} \max_x \underbrace{c_\text{rf} x_\text{rf}}_{\substack{\text{cost for borrowing}\\\text{or risk-free return}}} + \underbrace{\mu^\top x}_\text{expected return from stocks} \end{equation*}

```
[8]:
```

```
m.setObjective(c_rf * x_rf + mu.to_numpy() @ x, gp.GRB.MAXIMIZE)
```

We now solve the optimization problem:

```
[9]:
```

```
m.optimize()
```

```
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 1 rows, 463 columns and 463 nonzeros
```

```
Model fingerprint: 0xc5359fbf
```

```
Model has 1 quadratic constraint
```

```
Coefficient statistics:
```

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

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

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

```
Bounds range [3e-01, 1e+00]
```

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

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

```
Presolve time: 0.03s
```

```
Presolved: 464 rows, 926 columns, 107879 nonzeros
```

```
Presolved model has 1 second-order cone constraint
```

```
Ordering time: 0.01s
```

```
```

```
Barrier statistics:
```

```
AA' NZ : 1.074e+05
```

```
Factor NZ : 1.079e+05 (roughly 1 MB of memory)
```

```
Factor Ops : 3.341e+07 (less than 1 second per iteration)
```

```
Threads : 2
```

```
```

```
Objective Residual
```

```
Iter Primal Dual Primal Dual Compl Time
```

```
0 1.36004448e+01 1.52173653e-01 5.78e+01 6.79e-01 4.09e-02 0s
```

```
1 1.85351518e+00 1.75506108e+00 6.76e+00 7.47e-07 6.08e-03 0s
```

```
2 5.30037596e-01 9.57190813e-01 1.20e+00 1.38e-07 1.49e-03 0s
```

```
3 2.55706808e-01 5.45041563e-01 1.32e-06 2.05e-08 3.12e-04 0s
```

```
4 2.97986703e-01 4.47766523e-01 1.45e-12 4.74e-10 1.61e-04 0s
```

```
5 3.51518301e-01 3.94493222e-01 6.44e-15 1.58e-10 4.63e-05 0s
```

```
6 3.59709812e-01 3.65384265e-01 2.11e-14 2.21e-11 6.11e-06 0s
```

```
7 3.61347697e-01 3.61805875e-01 4.11e-15 1.22e-12 4.94e-07 0s
```

```
8 3.61595509e-01 3.61671962e-01 8.10e-14 6.36e-14 8.24e-08 0s
```

```
9 3.61660538e-01 3.61666698e-01 1.96e-12 1.94e-15 6.64e-09 0s
```

```
10 3.61665713e-01 3.61666153e-01 1.45e-11 2.44e-14 4.74e-10 0s
```

```
```

```
Barrier solved model in 10 iterations and 0.22 seconds (0.36 work units)
```

```
Optimal objective 3.61665713e-01
```

```
```

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

```
[10]:
```

```
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"Total investment: {x.X[x.X>1e-5].sum():.6f}")
print(f"Risk-free allocation: {x_rf.X:.6f}")
print(f"Number of positions: {np.count_nonzero(x.X[abs(x.X)>1e-5])}")
# Print all assets with a non-negligible position
df = pd.DataFrame(
index=mu.index,
data={
"x": x.X,
},
).round(6)
df[(abs(df["x"]) > 1e-5)].sort_values("x", ascending=False)
```

```
Expected return: 0.361666
Variance: 3.999998
Solution time: 0.22 seconds
Total investment: 1.180825
Risk-free allocation: -0.180830
Number of positions: 31
```

```
[10]:
```

x | |
---|---|

LLY | 0.234543 |

PGR | 0.130482 |

KDP | 0.109879 |

TMUS | 0.061029 |

NVDA | 0.059540 |

KR | 0.059145 |

DPZ | 0.058683 |

TTWO | 0.052532 |

WM | 0.050017 |

NOC | 0.049275 |

ODFL | 0.039827 |

ORLY | 0.035947 |

AVGO | 0.034790 |

WST | 0.029045 |

MSFT | 0.027447 |

ED | 0.019026 |

MKTX | 0.018752 |

AZO | 0.018154 |

MNST | 0.015003 |

CLX | 0.014767 |

META | 0.013703 |

HRL | 0.010081 |

NFLX | 0.009694 |

WMT | 0.008271 |

UNH | 0.007755 |

XEL | 0.004653 |

DXCM | 0.004488 |

CBOE | 0.003343 |

MOH | 0.000762 |

WEC | 0.000171 |

CME | 0.000020 |

## Comparison with the unconstrained portfolio without leverage#

We can also compute the portfolio without leverage and compare the resulting portfolios.

```
[11]:
```

```
# adjust RHS of short constraint
x_rf.lb = 0
x_rf.ub = 0
m.params.OutputFlag = 0
m.optimize()
# retrieve and display solution data
mask = (abs(df["x"]) > 1e-5) | (x.X > 1e-5)
df2 = pd.DataFrame(
index=df["x"][mask].index,
data={
"risk-free asset in [-0.3, 1]": df["x"],
"no risk-free asset": x.X[mask],
},
).sort_values(by=["risk-free asset in [-0.3, 1]"], ascending=True)
axs = df2.plot.barh(color=["#0b1a3c", "#dd2113"])
axs.set_xlabel("Fraction of investment sum")
plt.title("Minimum Variance portfolios with and without leverage")
plt.show()
```

## Efficient Frontiers#

The efficient frontier reveals the balance between risk and return in investment portfolios. It shows the best-expected return level that can be achieved for a specified risk level. We compute this by solving the above optimization problem for a sample of admissible risk levels with and without the risk-free asset. When we restrict investment amounts in the risk-free asset, that is \(u_\text{rf} < 1\), the model may be infeasible for very small risk levels.

```
[12]:
```

```
risks = np.linspace(0, 6, 30)
rf_bnds = [(0, 0), (0, 1), (-0.3, 0), (-1, -0.2)]
returns = pd.DataFrame(index=risks)
# prevent Gurobi log output
m.params.OutputFlag = 0
for lb, ub in rf_bnds:
name = f"[{lb}, {ub}]"
x_rf.LB = lb
x_rf.UB = ub
r = np.zeros(risks.shape)
# solve the model for each risk level
for i, risk_level in enumerate(risks):
# set risk level: RHS of risk constraint
risk_constr.QCRHS = risk_level**2
m.optimize()
# check status and store data
if m.Status == gp.GRB.OPTIMAL:
r[i] = m.ObjVal
else:
r[i] = float("NaN")
returns[name] = r
```

We can display the efficient frontiers for all strategies. We plot the expected returns (on the \(y\)-axis) against the standard deviation \(\sqrt{x^\top\Sigma x}\) of the expected returns (on the \(x\)-axis).

```
[13]:
```

```
colors = ["#0b1a3c", "#67a1c3", "#f6c105", "#dd2113"]
markers = ["o", "x", "x", "x"]
fig, axs = plt.subplots()
for column, color, marker in zip(returns.columns, colors, markers):
axs.scatter(x=returns.index, y=returns[column], marker=marker, color=color)
label = (
"without risk-free asset"
if column == "[0, 0]"
else f"risk-free asset in {column}"
)
axs.plot(
returns.index,
returns[column],
label=label,
color=color,
)
axs.set_xlabel("Standard deviation")
axs.set_ylabel("Expected return")
axs.legend()
axs.grid()
plt.show()
```

If we allow investing in a risk-free asset, the portfolio variance can be arbitrarily small. If one invests all capital into the risk-free asset, the variance (and standard deviation) is 0. Without that possibility (i.e., \(x_\text{rf}\leq 0\)), the minimal possible risk is greater than 0. If we can invest in the risk-free asset, the left-most part of the efficient frontier is a straight line from the portfolio that invests the entire capital into the risk-free asset to the minimal-variance portfolio. If we allow higher risk, including the ability to borrow cash, this shifts the efficient frontier towards higher returns.

## Takeaways#

Leverage can be modeled by adding a variable for the risk-free portion that can take negative values.

Different strategies can be tested by modifying bounds and right-hand sides; there is no need to rebuild the model.