Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions optimization/05_portfolio_optimization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "cvxpy==1.6.0",
# "marimo",
# "matplotlib==3.10.0",
# "numpy==2.2.2",
# "scipy==1.15.1",
# "wigglystuff==0.1.9",
# ]
# ///

import marimo

__generated_with = "0.11.2"
app = marimo.App()


@app.cell(hide_code=True)
def _(mo):
mo.md(r"""# Portfolio optimization""")
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
In this example we show how to use CVXPY to design a financial portfolio; this is called _portfolio optimization_.

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$. The goal is to maximize return of the portfolio while minimizing risk.
"""
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
## Asset returns and risk

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$. In real-world applications, $\mu$ and $\Sigma$ are estimated from data and models, and $w$ is chosen using a library like CVXPY.

${\bf E}R$ is the (mean) *return* of the portfolio. ${\bf var}(R)$ is the *risk* of the portfolio. Portfolio optimization has two competing objectives: high return and low risk.
"""
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
## Classical (Markowitz) portfolio optimization

Classical (Markowitz) portfolio optimization solves the optimization problem
"""
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
$$
\begin{array}{ll} \text{maximize} & \mu^T w - \gamma w^T\Sigma w\\
\text{subject to} & {\bf 1}^T w = 1, w \geq 0,
\end{array}
$$
"""
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
where $w \in {\bf R}^n$ is the optimization variable and $\gamma >0$ is a constant called the *risk aversion parameter*. The constraint $\mathbf{1}^Tw = 1$ says the portfolio weight vector must sum to 1, and $w \geq 0$ says that we can't invest a negative amount into any asset.

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.
"""
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
## Example

In the following code we compute and plot the optimal risk-return trade-off for $10$ assets. First we generate random problem data $\mu$ and $\Sigma$.
"""
)
return


@app.cell
def _():
import numpy as np
return (np,)


@app.cell(hide_code=True)
def _(mo, np):
import wigglystuff

mu_widget = mo.ui.anywidget(
wigglystuff.Matrix(
np.array(
[
[1.6],
[0.6],
[0.5],
[1.1],
[0.9],
[2.3],
[1.7],
[0.7],
[0.9],
[0.3],
]
)
)
)


mo.md(
rf"""
The value of $\mu$ is

{mu_widget.center()}

_Try changing the entries of $\mu$ and see how the plots below change._
"""
)
return mu_widget, wigglystuff


@app.cell
def _(mu_widget, np):
np.random.seed(1)
n = 10
mu = np.array(mu_widget.matrix)
Sigma = np.random.randn(n, n)
Sigma = Sigma.T.dot(Sigma)
return Sigma, mu, n


@app.cell(hide_code=True)
def _(mo):
mo.md("""Next, we solve the problem for 100 different values of $\gamma$""")
return


@app.cell
def _(Sigma, mu, n):
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])
return cp, gamma, prob, ret, risk, w


@app.cell
def _(cp, gamma, np, prob, ret, risk):
_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
return gamma_vals, ret_data, risk_data


@app.cell(hide_code=True)
def _(mo):
mo.md("""Plotted below are the risk return tradeoffs for two values of $\gamma$ (blue squares), and the risk return tradeoffs for investing fully in each asset (red circles)""")
return


@app.cell(hide_code=True)
def _(Sigma, cp, gamma_vals, mu, n, ret_data, risk_data):
import matplotlib.pyplot as plt

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(
"$\\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()
return ax, fig, marker, markers_on, plt


@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
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.
"""
)
return


@app.cell(hide_code=True)
def _(gamma, gamma_vals, markers_on, np, plt, prob, ret, risk):
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="$\\gamma = %.2f$" % gamma.value,
)
plt.xlabel("Return")
plt.ylabel("Density")
plt.legend(loc="upper right")
plt.show()
return midx, spstats, x