Akshay Agrawal commited on
Commit
f40980f
·
unverified ·
2 Parent(s): cef6f59 9655453

Merge pull request #27 from marimo-team/aka/portfolio_optimization.py

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