Skip to content

Commit 21d2df3

Browse files
committed
portfolio optimization example
1 parent cef6f59 commit 21d2df3

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# /// script
2+
# requires-python = ">=3.13"
3+
# dependencies = [
4+
# "cvxpy==1.6.0",
5+
# "marimo",
6+
# "matplotlib==3.10.0",
7+
# "numpy==2.2.2",
8+
# "scipy==1.15.1",
9+
# "wigglystuff==0.1.9",
10+
# ]
11+
# ///
12+
13+
import marimo
14+
15+
__generated_with = "0.11.2"
16+
app = marimo.App()
17+
18+
19+
@app.cell(hide_code=True)
20+
def _(mo):
21+
mo.md(r"""# Portfolio optimization""")
22+
return
23+
24+
25+
@app.cell(hide_code=True)
26+
def _(mo):
27+
mo.md(
28+
r"""
29+
In this example we show how to use CVXPY to design a financial portfolio; this is called _portfolio optimization_.
30+
31+
In portfolio optimization we have some amount of money to invest in any of $n$ different assets.
32+
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.
33+
"""
34+
)
35+
return
36+
37+
38+
@app.cell(hide_code=True)
39+
def _(mo):
40+
mo.md(
41+
r"""
42+
## Asset returns and risk
43+
44+
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$.
45+
46+
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$.
47+
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.
48+
49+
${\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.
50+
"""
51+
)
52+
return
53+
54+
55+
@app.cell(hide_code=True)
56+
def _(mo):
57+
mo.md(
58+
r"""
59+
## Classical (Markowitz) portfolio optimization
60+
61+
Classical (Markowitz) portfolio optimization solves the optimization problem
62+
"""
63+
)
64+
return
65+
66+
67+
@app.cell(hide_code=True)
68+
def _(mo):
69+
mo.md(
70+
r"""
71+
$$
72+
\begin{array}{ll} \text{maximize} & \mu^T w - \gamma w^T\Sigma w\\
73+
\text{subject to} & {\bf 1}^T w = 1, w \geq 0,
74+
\end{array}
75+
$$
76+
"""
77+
)
78+
return
79+
80+
81+
@app.cell(hide_code=True)
82+
def _(mo):
83+
mo.md(
84+
r"""
85+
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.
86+
87+
The objective $\mu^Tw - \gamma w^T\Sigma w$ is the *risk-adjusted return*. Varying $\gamma$ gives the optimal *risk-return trade-off*.
88+
We can get the same risk-return trade-off by fixing return and minimizing risk.
89+
"""
90+
)
91+
return
92+
93+
94+
@app.cell(hide_code=True)
95+
def _(mo):
96+
mo.md(
97+
r"""
98+
## Example
99+
100+
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$.
101+
"""
102+
)
103+
return
104+
105+
106+
@app.cell
107+
def _():
108+
import numpy as np
109+
return (np,)
110+
111+
112+
@app.cell(hide_code=True)
113+
def _(mo, np):
114+
import wigglystuff
115+
116+
mu_widget = mo.ui.anywidget(
117+
wigglystuff.Matrix(
118+
np.array(
119+
[
120+
[1.6],
121+
[0.6],
122+
[0.5],
123+
[1.1],
124+
[0.9],
125+
[2.3],
126+
[1.7],
127+
[0.7],
128+
[0.9],
129+
[0.3],
130+
]
131+
)
132+
)
133+
)
134+
135+
136+
mo.md(
137+
rf"""
138+
The value of $\mu$ is
139+
140+
{mu_widget.center()}
141+
142+
_Try changing the entries of $\mu$ and see how the plots below change._
143+
"""
144+
)
145+
return mu_widget, wigglystuff
146+
147+
148+
@app.cell
149+
def _(mu_widget, np):
150+
np.random.seed(1)
151+
n = 10
152+
mu = np.array(mu_widget.matrix)
153+
Sigma = np.random.randn(n, n)
154+
Sigma = Sigma.T.dot(Sigma)
155+
return Sigma, mu, n
156+
157+
158+
@app.cell(hide_code=True)
159+
def _(mo):
160+
mo.md("""Next, we solve the problem for 100 different values of $\gamma$""")
161+
return
162+
163+
164+
@app.cell
165+
def _(Sigma, mu, n):
166+
import cvxpy as cp
167+
168+
w = cp.Variable(n)
169+
gamma = cp.Parameter(nonneg=True)
170+
ret = mu.T @ w
171+
risk = cp.quad_form(w, Sigma)
172+
prob = cp.Problem(cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, w >= 0])
173+
return cp, gamma, prob, ret, risk, w
174+
175+
176+
@app.cell
177+
def _(cp, gamma, np, prob, ret, risk):
178+
_SAMPLES = 100
179+
risk_data = np.zeros(_SAMPLES)
180+
ret_data = np.zeros(_SAMPLES)
181+
gamma_vals = np.logspace(-2, 3, num=_SAMPLES)
182+
for _i in range(_SAMPLES):
183+
gamma.value = gamma_vals[_i]
184+
prob.solve()
185+
risk_data[_i] = cp.sqrt(risk).value
186+
ret_data[_i] = ret.value
187+
return gamma_vals, ret_data, risk_data
188+
189+
190+
@app.cell(hide_code=True)
191+
def _(mo):
192+
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)""")
193+
return
194+
195+
196+
@app.cell(hide_code=True)
197+
def _(Sigma, cp, gamma_vals, mu, n, ret_data, risk_data):
198+
import matplotlib.pyplot as plt
199+
200+
markers_on = [29, 40]
201+
fig = plt.figure()
202+
ax = fig.add_subplot(111)
203+
plt.plot(risk_data, ret_data, "g-")
204+
for marker in markers_on:
205+
plt.plot(risk_data[marker], ret_data[marker], "bs")
206+
ax.annotate(
207+
"$\\gamma = %.2f$" % gamma_vals[marker],
208+
xy=(risk_data[marker] + 0.08, ret_data[marker] - 0.03),
209+
)
210+
for _i in range(n):
211+
plt.plot(cp.sqrt(Sigma[_i, _i]).value, mu[_i], "ro")
212+
plt.xlabel("Standard deviation")
213+
plt.ylabel("Return")
214+
plt.show()
215+
return ax, fig, marker, markers_on, plt
216+
217+
218+
@app.cell(hide_code=True)
219+
def _(mo):
220+
mo.md(
221+
r"""
222+
We plot below the return distributions for the two risk aversion values marked on the trade-off curve.
223+
Notice that the probability of a loss is near 0 for the low risk value and far above 0 for the high risk value.
224+
"""
225+
)
226+
return
227+
228+
229+
@app.cell(hide_code=True)
230+
def _(gamma, gamma_vals, markers_on, np, plt, prob, ret, risk):
231+
import scipy.stats as spstats
232+
233+
plt.figure()
234+
for midx, _idx in enumerate(markers_on):
235+
gamma.value = gamma_vals[_idx]
236+
prob.solve()
237+
x = np.linspace(-2, 5, 1000)
238+
plt.plot(
239+
x,
240+
spstats.norm.pdf(x, ret.value, risk.value),
241+
label="$\\gamma = %.2f$" % gamma.value,
242+
)
243+
plt.xlabel("Return")
244+
plt.ylabel("Density")
245+
plt.legend(loc="upper right")
246+
plt.show()
247+
return midx, spstats, x

0 commit comments

Comments
 (0)