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: AMD EPYC 7763 64-Core Processor, instruction set [SSE2|AVX|AVX2]
Thread count: 1 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.04s
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 : 1
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.26444573e-01 9.73208598e-01 1.18e+00 1.48e-07 1.50e-03 0s
3 2.58180913e-01 5.35781906e-01 1.30e-06 1.75e-08 2.99e-04 0s
4 2.74073099e-01 4.36667485e-01 1.43e-12 1.15e-09 1.75e-04 0s
5 3.46759821e-01 3.86258876e-01 2.22e-15 2.28e-10 4.26e-05 0s
6 3.60280539e-01 3.63495642e-01 1.05e-15 2.37e-11 3.46e-06 0s
7 3.61562696e-01 3.61702796e-01 6.49e-15 9.30e-13 1.51e-07 0s
8 3.61658821e-01 3.61673013e-01 7.21e-13 2.39e-14 1.53e-08 0s
9 3.61665753e-01 3.61665955e-01 6.12e-13 5.33e-15 2.18e-10 0s
Barrier solved model in 9 iterations and 0.25 seconds (0.57 work units)
Optimal objective 3.61665753e-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.999999
Solution time: 0.25 seconds
Total investment: 1.180790
Risk-free allocation: -0.180793
Number of positions: 31
```

```
[10]:
```

x | |
---|---|

LLY | 0.234378 |

PGR | 0.130469 |

KDP | 0.109984 |

TMUS | 0.061071 |

NVDA | 0.059577 |

KR | 0.059146 |

DPZ | 0.058659 |

TTWO | 0.052455 |

WM | 0.050070 |

NOC | 0.049313 |

ODFL | 0.039862 |

ORLY | 0.035956 |

AVGO | 0.034829 |

WST | 0.029040 |

MSFT | 0.027461 |

ED | 0.018846 |

MKTX | 0.018722 |

AZO | 0.018125 |

MNST | 0.015005 |

CLX | 0.014768 |

META | 0.013696 |

HRL | 0.010052 |

NFLX | 0.009693 |

WMT | 0.008259 |

UNH | 0.007706 |

DXCM | 0.004475 |

XEL | 0.004328 |

CBOE | 0.003246 |

MOH | 0.000780 |

WEC | 0.000628 |

CME | 0.000189 |

## 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.