Spaces:
Running
Running
Akshay Agrawal
commited on
Commit
·
21d2df3
1
Parent(s):
cef6f59
portfolio optimization example
Browse files
optimization/05_portfolio_optimization.py
ADDED
@@ -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
|