diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7200f10..ee8161e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,31 +1,26 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - id: fix-encoding-pragma - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.286' + rev: 'v0.8.4' hooks: - id: ruff - args: [ --fix, --exit-non-zero-on-fix ] + args: [ --fix, --exit-non-zero-on-fix, --ignore=F821] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.35.0 + rev: v0.43.0 hooks: - id: markdownlint-fix # MD013: line too long args: [--disable, MD013, "--"] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.26.3 + rev: 0.30.0 hooks: - id: check-dependabot args: ["--verbose"] diff --git a/book/docs/applications/notebooks/portfolio_optimization.ipynb b/book/docs/applications/notebooks/portfolio_optimization.ipynb index 5e452006..96583e36 100644 --- a/book/docs/applications/notebooks/portfolio_optimization.ipynb +++ b/book/docs/applications/notebooks/portfolio_optimization.ipynb @@ -27,35 +27,39 @@ "The allocation $w_i<0$ means a *short position* in asset $i$, or that we borrow shares to sell now that we must replace later.\n", "The allocation $w \\geq 0$ is a *long only* portfolio.\n", "The quantity" - ], - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "$$\n", - "\\|w \\|_1 = {\\mathbf 1}^T w_+ + {\\mathbf 1}^T w_-\n", - "$$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] + "source": [ + "$$\n", + "\\|w \\|_1 = {\\mathbf 1}^T w_+ + {\\mathbf 1}^T w_-\n", + "$$" + ] }, { "cell_type": "markdown", - "source": [ - "is known as *leverage*." - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "is known as *leverage*." + ] }, { "cell_type": "markdown", @@ -84,39 +88,44 @@ "## Classical (Markowitz) portfolio optimization\n", "\n", "Classical (Markowitz) portfolio optimization solves the optimization problem" - ], - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "$$\n", "\\begin{array}{ll} \\mbox{maximize} & \\mu^T w - \\gamma w^T\\Sigma w\\\\\n", "\\mbox{subject to} & {\\bf 1}^T w = 1, \\quad w \\in {\\cal W},\n", "\\end{array}\n", "$$" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "where $w \\in {\\bf R}^n$ is the optimization variable, $\\cal W$ is a set of allowed portfolios (e.g., ${\\cal W} = {\\bf R}_+^n$ for a long only portfolio), and $\\gamma >0$ is the *risk aversion parameter*.\n", "\n", "The objective $\\mu^Tw - \\gamma w^T\\Sigma w$ is the *risk-adjusted return*. Varying $\\gamma$ gives the optimal *risk-return trade-off*.\n", "We can get the same risk-return trade-off by fixing return and minimizing risk." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "markdown", @@ -130,6 +139,15 @@ { "cell_type": "code", "execution_count": 1, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Generate data for long only portfolio optimization.\n", @@ -141,17 +159,20 @@ "mu = np.abs(np.random.randn(n, 1))\n", "Sigma = np.random.randn(n, n)\n", "Sigma = Sigma.T.dot(Sigma)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 2, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 2, + }, "outputs": [], "source": [ "# Long only portfolio optimization.\n", @@ -163,18 +184,30 @@ "ret = mu.T @ w\n", "risk = cp.quad_form(w, Sigma)\n", "prob = cp.Problem(cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, w >= 0])" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 3, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 3, - "outputs": [], + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/_3/k_9k5d5n5zz57w7qfll9rzs40000gn/T/ipykernel_90023/3491241395.py:10: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n", + " ret_data[i] = ret.value\n" + ] + } + ], "source": [ "# Compute trade-off curve.\n", "SAMPLES = 100\n", @@ -186,26 +219,964 @@ " prob.solve()\n", " risk_data[i] = cp.sqrt(risk).value\n", " ret_data[i] = ret.value" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 4, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 4, + }, "outputs": [ { "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2022-06-04T10:33:09.059339\n image/svg+xml\n \n \n Matplotlib v3.5.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - }, - "metadata": { - "needs_background": "light" + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2024-12-31T09:12:10.171826\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] }, + "metadata": {}, "output_type": "display_data" } ], @@ -231,39 +1202,1072 @@ "plt.xlabel(\"Standard deviation\")\n", "plt.ylabel(\"Return\")\n", "plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "We plot below the return distributions for the two risk aversion values marked on the trade-off curve.\n", - "Notice that the probability of a loss is near 0 for the low risk value and far above 0 for the high risk value." - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "We plot below the return distributions for the two risk aversion values marked on the trade-off curve.\n", + "Notice that the probability of a loss is near 0 for the low risk value and far above 0 for the high risk value." + ] }, { "cell_type": "code", "execution_count": 5, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2022-06-04T10:33:09.450579\n image/svg+xml\n \n \n Matplotlib v3.5.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - }, - "metadata": { - "needs_background": "light" + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2024-12-31T09:12:10.203903\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] }, + "metadata": {}, "output_type": "display_data" } ], @@ -287,16 +2291,19 @@ "plt.ylabel(\"Density\")\n", "plt.legend(loc=\"upper right\")\n", "plt.show()" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { - "name": "#%%\n" + "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "### Portfolio constraints\n", "\n", @@ -306,32 +2313,38 @@ "Another interesting constraint is the *market neutral* constraint $m^T \\Sigma w =0$, where $m_i$ is the capitalization of asset $i$.\n", "$M = m^Tr$ is the *market return*, and $m^T \\Sigma w = {\\bf cov}(M,R)$.\n", "The market neutral constraint ensures that the portfolio return is uncorrelated with the market return." - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "### Example\n", "\n", "In the following code we compute and plot optimal risk-return trade-off curves for leverage limits of 1, 2, and 4.\n", "Notice that more leverage increases returns and allows greater risk." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "code", "execution_count": 6, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Portfolio optimization with leverage limit.\n", @@ -339,18 +2352,30 @@ "prob = cp.Problem(\n", " cp.Maximize(ret - gamma * risk), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax]\n", ")" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 7, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 7, - "outputs": [], + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/_3/k_9k5d5n5zz57w7qfll9rzs40000gn/T/ipykernel_90023/231315309.py:14: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n", + " ret_data[k, i] = ret.value\n" + ] + } + ], "source": [ "# Compute trade-off curve for each leverage limit.\n", "L_vals = [1, 2, 4]\n", @@ -366,26 +2391,1226 @@ " prob.solve(solver=cp.SCS)\n", " risk_data[k, i] = cp.sqrt(risk).value\n", " ret_data[k, i] = ret.value" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 8, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 8, + }, "outputs": [ { "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2022-06-04T10:33:10.581595\n image/svg+xml\n \n \n Matplotlib v3.5.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - }, - "metadata": { - "needs_background": "light" + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2024-12-31T09:12:10.379430\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] }, + "metadata": {}, "output_type": "display_data" } ], @@ -400,46 +3625,55 @@ "plt.ylabel(\"Return\")\n", "plt.legend(loc=\"lower right\")\n", "plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "We next examine the points on each trade-off curve where $w^T\\Sigma w = 2$.\n", - "We plot the amount of each asset held in each portfolio as bar graphs. (Negative holdings indicate a short position.)\n", - "Notice that some assets are held in a long position for the low leverage portfolio but in a short position in the higher leverage portfolios. " - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "We next examine the points on each trade-off curve where $w^T\\Sigma w = 2$.\n", + "We plot the amount of each asset held in each portfolio as bar graphs. (Negative holdings indicate a short position.)\n", + "Notice that some assets are held in a long position for the low leverage portfolio but in a short position in the higher leverage portfolios. " + ] }, { "cell_type": "code", "execution_count": 9, - "outputs": [], - "source": [ - "# Portfolio optimization with a leverage limit and a bound on risk.\n", - "prob = cp.Problem(cp.Maximize(ret), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax, risk <= 2])" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [ + "# Portfolio optimization with a leverage limit and a bound on risk.\n", + "prob = cp.Problem(cp.Maximize(ret), [cp.sum(w) == 1, cp.norm(w, 1) <= Lmax, risk <= 2])" + ] }, { "cell_type": "code", "execution_count": 10, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Compute solution for different leverage limits.\n", @@ -447,26 +3681,930 @@ " Lmax.value = L_val\n", " prob.solve()\n", " w_vals.append(w.value)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 11, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 11, + }, "outputs": [ { "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2022-06-04T10:33:10.858156\n image/svg+xml\n \n \n Matplotlib v3.5.2, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - }, - "metadata": { - "needs_background": "light" + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2024-12-31T09:12:10.429499\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] }, + "metadata": {}, "output_type": "display_data" } ], @@ -487,122 +4625,141 @@ "plt.xlim([1 - 0.375, 10 + 0.375])\n", "plt.xticks(np.arange(1, n + 1))\n", "plt.show()" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { - "name": "#%%\n" + "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "## Variations\n", "\n", "There are many more variations of classical portfolio optimization. We might require that $\\mu^T w \\geq R^\\mathrm{min}$ and minimize $w^T \\Sigma w$ or $\\|\\Sigma ^{1/2} w\\|_2$.\n", "We could include the (broker) cost of short positions as the penalty $s^T (w)_-$ for some $s \\geq 0$.\n", "We could include transaction costs (from a previous portfolio $w^\\mathrm{prev}$) as the penalty" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] - }, - { - "cell_type": "markdown", "source": [ "$$\n", "\\kappa ^T |w-w^\\mathrm{prev}|^\\eta, \\quad\n", "\\kappa \\geq 0.\n", "$$" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] + "source": [ + "Common values of $\\eta$ are $\\eta =1, ~ 3/2, ~2$." + ] }, { "cell_type": "markdown", - "source": [ - "Common values of $\\eta$ are $\\eta =1, ~ 3/2, ~2$." - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "## Factor covariance model\n", "\n", "A particularly common and useful variation is to model the covariance matrix $\\Sigma$ as a factor model" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] - }, - { - "cell_type": "markdown", "source": [ "$$\n", "\\Sigma = F \\tilde \\Sigma F^T + D,\n", "$$" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] - }, - { - "cell_type": "markdown", "source": [ "\n", "where $F \\in {\\bf R}^{n \\times k}$, $k \\ll n$ is the *factor loading matrix*. $k$ is the number of factors (or sectors) (typically 10s). $F_{ij}$ is the loading of asset $i$ to factor $j$.\n", "$D$ is a diagonal matrix; $D_{ii}>0$ is the *idiosyncratic risk*. $\\tilde \\Sigma > 0$ is the *factor covariance matrix*.\n", "\n", "$F^Tw \\in {\\bf R}^k$ gives the portfolio *factor exposures*. A portfolio is *factor $j$ neutral* if $(F^Tw)_j=0$.\n" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "## Portfolio optimization with factor covariance model\n", "\n", "Using the factor covariance model, we frame the portfolio optimization problem as" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } }, - "outputs": [] - }, - { - "cell_type": "markdown", "source": [ "$$\n", "\\begin{array}{ll} \\mbox{maximize} & \\mu^T w - \\gamma \\left(f^T \\tilde \\Sigma f + w^TDw \\right) \\\\\n", @@ -610,30 +4767,36 @@ "& w \\in {\\cal W}, \\quad f \\in {\\cal F},\n", "\\end{array}\n", "$$" - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "where the variables are the allocations $w \\in {\\bf R}^n$ and factor exposures $f\\in {\\bf R}^k$ and $\\cal F$ gives the factor exposure constraints.\n", "\n", "Using the factor covariance model in the optimization problem has a computational advantage. The solve time is $O(nk^2)$ versus $O(n^3)$ for the standard problem." - ], + ] + }, + { + "cell_type": "markdown", "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "markdown", + }, "source": [ "## Example\n", "\n", @@ -641,17 +4804,20 @@ "We set the leverage limit $=2$ and $\\gamma=0.1$.\n", "\n", "We solve the problem both with the covariance given as a single matrix and as a factor model." - ], + ] + }, + { + "cell_type": "code", + "execution_count": 12, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { - "name": "#%% md\n" + "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, + }, "outputs": [], "source": [ "# Generate data for factor model.\n", @@ -663,18 +4829,100 @@ "Sigma_tilde = Sigma_tilde.T.dot(Sigma_tilde)\n", "D = sp.diags(np.random.uniform(0, 0.9, size=n))\n", "F = np.random.randn(n, m)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 13, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "===============================================================================\n", + " CVXPY \n", + " v1.6.0 \n", + "===============================================================================\n", + "(CVXPY) Dec 31 09:12:10 AM: Your problem has 630 variables, 32 constraints, and 2 parameters.\n", + "(CVXPY) Dec 31 09:12:10 AM: It is compliant with the following grammars: DCP, DQCP\n", + "(CVXPY) Dec 31 09:12:10 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.\n", + "(CVXPY) Dec 31 09:12:10 AM: Your problem is compiled with the CPP canonicalization backend.\n", + "-------------------------------------------------------------------------------\n", + " Compilation \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:10 AM: Compiling problem (target solver=OSQP).\n", + "(CVXPY) Dec 31 09:12:10 AM: Reduction chain: FlipObjective -> CvxAttr2Constr -> Qp2SymbolicQp -> QpMatrixStuffing -> OSQP\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction FlipObjective\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction CvxAttr2Constr\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction Qp2SymbolicQp\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction QpMatrixStuffing\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction OSQP\n", + "(CVXPY) Dec 31 09:12:10 AM: Finished problem compilation (took 8.096e-03 seconds).\n", + "(CVXPY) Dec 31 09:12:10 AM: (Subsequent compilations of this problem, using the same arguments, should take less time.)\n", + "-------------------------------------------------------------------------------\n", + " Numerical solver \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:10 AM: Invoking solver OSQP to obtain a solution.\n", + "-----------------------------------------------------------------\n", + " OSQP v0.6.3 - Operator Splitting QP Solver\n", + " (c) Bartolomeo Stellato, Goran Banjac\n", + " University of Oxford - Stanford University 2021\n", + "-----------------------------------------------------------------\n", + "problem: variables n = 1830, constraints m = 1832\n", + " nnz(P) + nnz(A) = 23895\n", + "settings: linear system solver = qdldl,\n", + " eps_abs = 1.0e-05, eps_rel = 1.0e-05,\n", + " eps_prim_inf = 1.0e-04, eps_dual_inf = 1.0e-04,\n", + " rho = 1.00e-01 (adaptive),\n", + " sigma = 1.00e-06, alpha = 1.60, max_iter = 10000\n", + " check_termination: on (interval 25),\n", + " scaling: on, scaled_termination: off\n", + " warm start: on, polish: on, time_limit: off\n", + "\n", + "iter objective pri res dua res rho time\n", + " 1 -4.3898e+02 7.14e+00 3.28e+02 1.00e-01 1.85e-03s\n", + " 200 -4.0679e+00 2.07e-03 7.49e-04 1.00e-01 7.88e-03s\n", + " 400 -4.0680e+00 4.21e-04 6.77e-04 5.05e-01 1.43e-02s\n", + " 600 -4.0399e+00 1.04e-04 1.64e-04 5.05e-01 2.06e-02s\n", + " 800 -4.0344e+00 4.08e-05 1.19e-04 5.05e-01 2.69e-02s\n", + " 875 -4.0351e+00 2.76e-05 2.62e-05 5.05e-01 2.92e-02s\n", + "\n", + "status: solved\n", + "solution polish: unsuccessful\n", + "number of iterations: 875\n", + "optimal objective: -4.0351\n", + "run time: 3.06e-02s\n", + "optimal rho estimate: 6.34e-01\n", + "\n", + "-------------------------------------------------------------------------------\n", + " Summary \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:10 AM: Problem status: optimal\n", + "(CVXPY) Dec 31 09:12:10 AM: Optimal value: 4.035e+00\n", + "(CVXPY) Dec 31 09:12:10 AM: Compilation took 8.096e-03 seconds\n", + "(CVXPY) Dec 31 09:12:10 AM: Solver (including time spent in interface) took 3.174e-02 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "4.035127190691267" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Factor model portfolio optimization.\n", "w = cp.Variable(n)\n", @@ -692,18 +4940,193 @@ "Lmax.value = 2\n", "gamma.value = 0.1\n", "prob_factor.solve(verbose=True)" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 14, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "===============================================================================\n", + " CVXPY \n", + " v1.6.0 \n", + "===============================================================================\n", + "(CVXPY) Dec 31 09:12:10 AM: Your problem has 600 variables, 2 constraints, and 2 parameters.\n", + "(CVXPY) Dec 31 09:12:10 AM: It is compliant with the following grammars: DCP, DQCP\n", + "(CVXPY) Dec 31 09:12:10 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution.\n", + "(CVXPY) Dec 31 09:12:10 AM: Your problem is compiled with the CPP canonicalization backend.\n", + "-------------------------------------------------------------------------------\n", + " Compilation \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:10 AM: Compiling problem (target solver=OSQP).\n", + "(CVXPY) Dec 31 09:12:10 AM: Reduction chain: FlipObjective -> CvxAttr2Constr -> Qp2SymbolicQp -> QpMatrixStuffing -> OSQP\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction FlipObjective\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction CvxAttr2Constr\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction Qp2SymbolicQp\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction QpMatrixStuffing\n", + "(CVXPY) Dec 31 09:12:10 AM: Applying reduction OSQP\n", + "(CVXPY) Dec 31 09:12:10 AM: Finished problem compilation (took 4.447e-02 seconds).\n", + "(CVXPY) Dec 31 09:12:10 AM: (Subsequent compilations of this problem, using the same arguments, should take less time.)\n", + "-------------------------------------------------------------------------------\n", + " Numerical solver \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:10 AM: Invoking solver OSQP to obtain a solution.\n", + "-----------------------------------------------------------------\n", + " OSQP v0.6.3 - Operator Splitting QP Solver\n", + " (c) Bartolomeo Stellato, Goran Banjac\n", + " University of Oxford - Stanford University 2021\n", + "-----------------------------------------------------------------\n", + "problem: variables n = 1200, constraints m = 1202\n", + " nnz(P) + nnz(A) = 183900\n", + "settings: linear system solver = qdldl,\n", + " eps_abs = 1.0e-05, eps_rel = 1.0e-05,\n", + " eps_prim_inf = 1.0e-04, eps_dual_inf = 1.0e-04,\n", + " rho = 1.00e-01 (adaptive),\n", + " sigma = 1.00e-06, alpha = 1.60, max_iter = 100000\n", + " check_termination: on (interval 25),\n", + " scaling: on, scaled_termination: off\n", + " warm start: on, polish: on, time_limit: off\n", + "\n", + "iter objective pri res dua res rho time\n", + " 1 -1.8064e+03 7.86e+01 4.95e+03 1.00e-01 2.00e-02s\n", + " 200 -2.9381e+01 7.60e-02 2.60e-04 1.00e-01 6.45e-02s\n", + " 400 -1.3664e+01 3.22e-02 7.62e-05 1.00e-01 1.09e-01s\n", + " 600 -8.4293e+00 1.81e-02 3.21e-05 1.00e-01 1.55e-01s\n", + " 800 -6.2482e+00 1.09e-02 1.53e-05 1.00e-01 2.00e-01s\n", + "1000 -5.1966e+00 7.80e-03 7.36e-06 1.00e-01 2.44e-01s\n", + "1200 -4.6819e+00 6.08e-03 3.70e-06 1.00e-01 2.90e-01s\n", + "1400 -4.4246e+00 5.08e-03 1.96e-06 1.00e-01 3.35e-01s\n", + "1600 -4.2907e+00 4.45e-03 1.11e-06 1.00e-01 3.79e-01s\n", + "1800 -4.2168e+00 4.18e-03 6.81e-07 1.00e-01 4.24e-01s\n", + "2000 -4.1406e+00 3.80e-03 2.16e-05 5.10e-01 4.85e-01s\n", + "2200 -4.1575e+00 2.36e-03 1.11e-05 5.10e-01 5.30e-01s\n", + "2400 -4.1021e+00 2.01e-03 1.66e-05 5.10e-01 5.77e-01s\n", + "2600 -4.1081e+00 1.75e-03 2.84e-06 5.10e-01 6.23e-01s\n", + "2800 -4.1103e+00 1.73e-03 6.02e-07 5.10e-01 6.68e-01s\n", + "3000 -4.1054e+00 1.58e-03 7.73e-06 5.10e-01 7.13e-01s\n", + "3200 -4.1026e+00 1.45e-03 4.15e-06 5.10e-01 7.59e-01s\n", + "3400 -4.1038e+00 1.25e-03 1.93e-05 5.10e-01 8.06e-01s\n", + "3600 -4.0936e+00 9.19e-04 5.72e-06 5.10e-01 8.52e-01s\n", + "3800 -4.0887e+00 8.78e-04 3.79e-06 5.10e-01 8.98e-01s\n", + "4000 -4.0848e+00 8.77e-04 2.82e-06 5.10e-01 9.44e-01s\n", + "4200 -4.0863e+00 8.16e-04 2.36e-06 5.10e-01 9.90e-01s\n", + "4400 -4.0854e+00 7.30e-04 2.40e-06 5.10e-01 1.03e+00s\n", + "4600 -4.0858e+00 7.02e-04 9.78e-07 5.10e-01 1.08e+00s\n", + "4800 -4.0860e+00 6.88e-04 5.74e-07 5.10e-01 1.13e+00s\n", + "5000 -4.0857e+00 6.76e-04 4.10e-07 5.10e-01 1.17e+00s\n", + "5200 -4.0852e+00 6.66e-04 3.36e-07 5.10e-01 1.22e+00s\n", + "5400 -4.0848e+00 6.57e-04 2.77e-07 5.10e-01 1.26e+00s\n", + "5600 -4.0875e+00 5.79e-04 1.34e-06 5.10e-01 1.31e+00s\n", + "5800 -4.0823e+00 5.60e-04 7.75e-07 5.10e-01 1.35e+00s\n", + "6000 -4.0792e+00 5.43e-04 5.54e-07 5.10e-01 1.40e+00s\n", + "6200 -4.0770e+00 5.27e-04 4.39e-07 5.10e-01 1.44e+00s\n", + "6400 -4.0752e+00 5.13e-04 3.83e-07 5.10e-01 1.49e+00s\n", + "6600 -4.0740e+00 5.03e-04 3.87e-07 5.10e-01 1.53e+00s\n", + "6800 -4.0731e+00 4.92e-04 3.09e-07 5.10e-01 1.58e+00s\n", + "7000 -4.0722e+00 4.82e-04 2.81e-07 5.10e-01 1.62e+00s\n", + "7200 -4.0714e+00 4.76e-04 2.47e-07 5.10e-01 1.67e+00s\n", + "7400 -4.0707e+00 4.71e-04 2.16e-07 5.10e-01 1.71e+00s\n", + "7600 -4.0701e+00 4.66e-04 1.89e-07 5.10e-01 1.76e+00s\n", + "7800 -4.0696e+00 4.61e-04 1.67e-07 5.10e-01 1.80e+00s\n", + "8000 -4.0692e+00 4.56e-04 1.47e-07 5.10e-01 1.85e+00s\n", + "8200 -4.0687e+00 4.52e-04 1.31e-07 5.10e-01 1.89e+00s\n", + "8400 -4.0683e+00 4.48e-04 1.18e-07 5.10e-01 1.94e+00s\n", + "8600 -4.0680e+00 4.43e-04 1.07e-07 5.10e-01 1.98e+00s\n", + "8800 -4.0676e+00 4.39e-04 9.71e-08 5.10e-01 2.03e+00s\n", + "9000 -4.0673e+00 4.35e-04 8.91e-08 5.10e-01 2.07e+00s\n", + "9200 -4.0670e+00 4.31e-04 8.24e-08 5.10e-01 2.12e+00s\n", + "9400 -4.0667e+00 4.27e-04 7.68e-08 5.10e-01 2.17e+00s\n", + "9600 -4.0664e+00 4.23e-04 7.20e-08 5.10e-01 2.21e+00s\n", + "9800 -4.0662e+00 4.20e-04 6.80e-08 5.10e-01 2.26e+00s\n", + "10000 -4.0659e+00 4.16e-04 6.46e-08 5.10e-01 2.30e+00s\n", + "10200 -4.0609e+00 3.77e-04 8.45e-07 2.69e-01 2.38e+00s\n", + "10400 -4.0568e+00 3.37e-04 7.04e-07 2.69e-01 2.42e+00s\n", + "10600 -4.0546e+00 3.02e-04 5.55e-07 2.69e-01 2.47e+00s\n", + "10800 -4.0529e+00 2.72e-04 4.51e-07 2.69e-01 2.51e+00s\n", + "11000 -4.0515e+00 2.46e-04 3.74e-07 2.69e-01 2.56e+00s\n", + "11200 -4.0502e+00 2.24e-04 3.15e-07 2.69e-01 2.60e+00s\n", + "11400 -4.0491e+00 2.05e-04 2.67e-07 2.69e-01 2.65e+00s\n", + "11600 -4.0481e+00 1.88e-04 2.29e-07 2.69e-01 2.69e+00s\n", + "11800 -4.0472e+00 1.74e-04 1.97e-07 2.69e-01 2.74e+00s\n", + "12000 -4.0464e+00 1.64e-04 1.70e-07 2.69e-01 2.78e+00s\n", + "12200 -4.0456e+00 1.59e-04 1.47e-07 2.69e-01 2.83e+00s\n", + "12400 -4.0450e+00 1.55e-04 1.28e-07 2.69e-01 2.87e+00s\n", + "12600 -4.0444e+00 1.51e-04 1.12e-07 2.69e-01 2.92e+00s\n", + "12800 -4.0438e+00 1.48e-04 9.81e-08 2.69e-01 2.96e+00s\n", + "13000 -4.0433e+00 1.44e-04 8.65e-08 2.69e-01 3.01e+00s\n", + "13200 -4.0429e+00 1.42e-04 7.65e-08 2.69e-01 3.05e+00s\n", + "13400 -4.0425e+00 1.39e-04 6.79e-08 2.69e-01 3.10e+00s\n", + "13600 -4.0421e+00 1.37e-04 6.04e-08 2.69e-01 3.14e+00s\n", + "13800 -4.0418e+00 1.35e-04 5.39e-08 2.69e-01 3.19e+00s\n", + "14000 -4.0415e+00 1.33e-04 4.82e-08 2.69e-01 3.23e+00s\n", + "14200 -4.0412e+00 1.31e-04 4.32e-08 2.69e-01 3.28e+00s\n", + "14400 -4.0409e+00 1.29e-04 3.88e-08 2.69e-01 3.32e+00s\n", + "14600 -4.0407e+00 1.28e-04 3.49e-08 2.69e-01 3.37e+00s\n", + "14800 -4.0404e+00 1.26e-04 3.16e-08 2.69e-01 3.41e+00s\n", + "15000 -4.0402e+00 1.25e-04 2.86e-08 2.69e-01 3.46e+00s\n", + "15200 -4.0401e+00 1.24e-04 2.59e-08 2.69e-01 3.50e+00s\n", + "15400 -4.0399e+00 1.23e-04 2.36e-08 2.69e-01 3.55e+00s\n", + "15600 -4.0397e+00 1.21e-04 2.15e-08 2.69e-01 3.59e+00s\n", + "15800 -4.0396e+00 1.20e-04 1.97e-08 2.69e-01 3.64e+00s\n", + "16000 -4.0393e+00 1.18e-04 4.25e-07 1.35e+00 3.69e+00s\n", + "16200 -4.0388e+00 1.14e-04 2.85e-07 1.35e+00 3.74e+00s\n", + "16400 -4.0385e+00 1.10e-04 2.07e-07 1.35e+00 3.79e+00s\n", + "16600 -4.0382e+00 1.07e-04 1.61e-07 1.35e+00 3.83e+00s\n", + "16800 -4.0380e+00 1.03e-04 1.33e-07 1.35e+00 3.88e+00s\n", + "17000 -4.0378e+00 1.00e-04 1.15e-07 1.35e+00 3.92e+00s\n", + "17200 -4.0377e+00 9.71e-05 1.03e-07 1.35e+00 3.97e+00s\n", + "17400 -4.0376e+00 9.41e-05 9.48e-08 1.35e+00 4.02e+00s\n", + "17600 -4.0368e+00 8.10e-05 1.65e-06 1.35e+00 4.06e+00s\n", + "17800 -4.0361e+00 7.06e-05 8.13e-07 1.35e+00 4.11e+00s\n", + "18000 -4.0358e+00 6.45e-05 6.02e-07 1.35e+00 4.15e+00s\n", + "18200 -4.0356e+00 6.02e-05 4.83e-07 1.35e+00 4.20e+00s\n", + "18400 -4.0355e+00 5.62e-05 3.97e-07 1.35e+00 4.24e+00s\n", + "18600 -4.0345e+00 4.58e-05 9.10e-07 1.35e+00 4.29e+00s\n", + "18800 -4.0345e+00 4.07e-05 3.93e-07 1.35e+00 4.33e+00s\n", + "19000 -4.0346e+00 3.71e-05 2.56e-07 1.35e+00 4.38e+00s\n", + "19200 -4.0347e+00 3.41e-05 1.82e-07 1.35e+00 4.42e+00s\n", + "19400 -4.0348e+00 3.15e-05 1.38e-07 1.35e+00 4.47e+00s\n", + "19550 -4.0348e+00 2.98e-05 1.27e-07 1.35e+00 4.50e+00s\n", + "\n", + "status: solved\n", + "solution polish: unsuccessful\n", + "number of iterations: 19550\n", + "optimal objective: -4.0348\n", + "run time: 4.52e+00s\n", + "optimal rho estimate: 1.29e+00\n", + "\n", + "-------------------------------------------------------------------------------\n", + " Summary \n", + "-------------------------------------------------------------------------------\n", + "(CVXPY) Dec 31 09:12:15 AM: Problem status: optimal\n", + "(CVXPY) Dec 31 09:12:15 AM: Optimal value: 4.035e+00\n", + "(CVXPY) Dec 31 09:12:15 AM: Compilation took 4.447e-02 seconds\n", + "(CVXPY) Dec 31 09:12:15 AM: Solver (including time spent in interface) took 4.527e+00 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "4.034822208633644" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from cvxpy.atoms.affine.wraps import psd_wrap\n", "\n", @@ -714,59 +5137,56 @@ ")\n", "\n", "prob.solve(verbose=True, max_iter=100_000)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "We expect the factor model to be at least 20 times faster than the standard model as\n", - "$n^2 = 20 n m$ where $n$ is the number of assets and $m$ is the number of factors.\n", - "This estimate does not yet reflect a smaller number of iterations required for the factor model." - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], + }, "source": [ - "print(\"Factor model solve time = {}\".format(prob_factor.solver_stats.solve_time))\n", - "print(\"Single model solve time = {}\".format(prob.solver_stats.solve_time))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + "We expect the factor model to be at least 20 times faster than the standard model as\n", + "$n^2 = 20 n m$ where $n$ is the number of assets and $m$ is the number of factors.\n", + "This estimate does not yet reflect a smaller number of iterations required for the factor model." + ] }, { "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [], + "execution_count": 15, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Factor model solve time = 0.030579998999999997\n", + "Single model solve time = 4.521940166\n" + ] + } + ], + "source": [ + "print(\"Factor model solve time = {}\".format(prob_factor.solver_stats.solve_time))\n", + "print(\"Single model solve time = {}\".format(prob.solver_stats.solve_time))" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -780,9 +5200,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/13.20.ipynb b/book/docs/exercises/notebooks/13.20.ipynb index 020a67cd..78ebac27 100644 --- a/book/docs/exercises/notebooks/13.20.ipynb +++ b/book/docs/exercises/notebooks/13.20.ipynb @@ -30,40 +30,43 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "$$\n", "\\rho_i = \\frac{\\partial \\log R(x)}{\\partial \\log x_i} =\n", "\\frac{\\partial R(x)}{R(x)} \\frac{x_i}{\\partial x_i}, \\quad i=1, \\ldots, n.\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "Thus $\\rho_i$ gives the fractional increase in risk per fractional increase\n", "in investment $i$.\n", "We can express the risk contributions as" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "$$\n", "\\rho_i = \\frac{x_i (\\Sigma x)_i} {x^T\\Sigma x}, \\quad i=1, \\ldots, n,\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "from which we see that $\\sum_{i=1}^n \\rho_i = 1$.\n", "For general $x$, we can have $\\rho_i <0$, which means that a small increase\n", @@ -88,10 +91,7 @@ "\n", "*Hint.*\n", "Minimize $(1/2)x^T\\Sigma x - \\sum_{i=1}^n \\rho_i^\\mathrm{des} \\log x_i$." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -118,7 +118,6 @@ "outputs": [], "source": [ "import numpy as np\n", - "import cvxpy as cp\n", "\n", "Sigma = np.array(\n", " np.matrix(\n", diff --git a/book/docs/exercises/notebooks/13.21.ipynb b/book/docs/exercises/notebooks/13.21.ipynb index 1cd75d24..2445f9f6 100644 --- a/book/docs/exercises/notebooks/13.21.ipynb +++ b/book/docs/exercises/notebooks/13.21.ipynb @@ -35,6 +35,12 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "$$\n", " \\begin{array}{ll}\n", @@ -43,13 +49,16 @@ " & \\mathbb{1}^Tw = 1,\n", " \\end{array}\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "where $w\\in \\mathbb{R}^n$ is the variable, $\\mu$ is the mean return,\n", "$\\Sigma\\in \\mathbb{S}_{++}^n$ is the return covariance, and $\\gamma>0$ is the\n", @@ -60,13 +69,16 @@ "portfolio by buying and selling assets.\n", "We call the post-trade portfolio weights $\\tilde{w}_t$.\n", "They are found by solving the (rebalancing) problem" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "$$\n", " \\begin{array}{ll}\n", @@ -75,13 +87,16 @@ " & \\mathbb{1}^Tw = 1,\n", " \\end{array}\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "with variable $w \\in \\mathbb{R}^n$,\n", "where $\\kappa\\in\\mathbb{R}^n_+$ is the vector of (so-called linear) transaction costs\n", @@ -112,10 +127,7 @@ "The standard model is that $r_t$ are IID random variables with\n", "mean and covariance $\\mu$ and $\\Sigma$,\n", "but this is not relevant in this problem." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -166,16 +178,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "pycharm": { "name": "#%%\n" } }, - "outputs": [], + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'ws' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 38\u001b[0m\n\u001b[1;32m 36\u001b[0m plt\u001b[38;5;241m.\u001b[39mfigure(figsize\u001b[38;5;241m=\u001b[39m(\u001b[38;5;241m13\u001b[39m, \u001b[38;5;241m5\u001b[39m))\n\u001b[1;32m 37\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m j \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(n):\n\u001b[0;32m---> 38\u001b[0m plt\u001b[38;5;241m.\u001b[39mplot(\u001b[38;5;28mrange\u001b[39m(T), \u001b[43mws\u001b[49m[:, j], colors[j])\n\u001b[1;32m 39\u001b[0m plt\u001b[38;5;241m.\u001b[39mplot(\u001b[38;5;28mrange\u001b[39m(T), [w_star[j]] \u001b[38;5;241m*\u001b[39m T, colors[j] \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m--\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 40\u001b[0m non_zero_trades \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mabs\u001b[39m(us[:, j]) \u001b[38;5;241m>\u001b[39m threshold\n", + "\u001b[0;31mNameError\u001b[0m: name 'ws' is not defined" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# data and code for multiperiod portfolio rebalancing problem\n", "import numpy as np\n", + "import matplotlib.pyplot as plt\n", "\n", "T = 100\n", "n = 5\n", @@ -196,7 +230,7 @@ "\n", "## Generate returns\n", "# call this function to generate a vector r of market returns\n", - "generateReturns = lambda: np.random.multivariate_normal(mu, Sigma)\n", + "# generateReturns = np.random.multivariate_normal(mu, Sigma)\n", "\n", "## Plotting code\n", "# You must provide three objects:\n", @@ -206,7 +240,6 @@ "# the trades at each period: w_t_tilde - w_t;\n", "# - w_star: np.array of size n,\n", "# the \"target\" solution w_star.\n", - "import matplotlib.pyplot as plt\n", "\n", "colors = [\"b\", \"r\", \"g\", \"c\", \"m\"]\n", "plt.figure(figsize=(13, 5))\n", @@ -224,6 +257,9 @@ "execution_count": null, "metadata": { "collapsed": true, + "jupyter": { + "outputs_hidden": true + }, "pycharm": { "name": "#%%\n" } @@ -234,7 +270,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -248,9 +284,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/17.3.ipynb b/book/docs/exercises/notebooks/17.3.ipynb index 46f02189..30203572 100644 --- a/book/docs/exercises/notebooks/17.3.ipynb +++ b/book/docs/exercises/notebooks/17.3.ipynb @@ -13,6 +13,12 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "Flux balance analysis is based on a very simple model of\n", "the reactions going on in a cell, keeping track only of the gross rate of consumption and production\n", @@ -50,10 +56,7 @@ "$$\n", "Sv = 0, \\quad v \\succeq 0, \\quad v \\preceq v^\\mathrm{max}.\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -175,13 +178,14 @@ }, "outputs": [], "source": [ + "# flake8: noqa: F821\n", "# Check essential genes and synthetic lethals\n", "G = np.zeros((n, n))\n", "for i in range(n):\n", " for j in range(i, n):\n", " # Taking out genes i and j (i can equal j)\n", - " # TODO: your code here\n", - "\n", + " # TODO: replace pass with your code here, \n", + " pass\n", "\n", "print(G < 0.2*Gstar)" ] @@ -203,9 +207,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/3.20.ipynb b/book/docs/exercises/notebooks/3.20.ipynb index a6cf2536..24f3b94e 100644 --- a/book/docs/exercises/notebooks/3.20.ipynb +++ b/book/docs/exercises/notebooks/3.20.ipynb @@ -13,6 +13,12 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "A vehicle (say, an airplane) travels along a fixed path of $n$\n", "segments, between $n+1$ waypoints labeled $0, \\ldots, n$. Segment $i$ starts at waypoint $i-1$ and\n", @@ -28,10 +34,7 @@ "You are given the data $d$ (segment travel distances), $s^\\mathrm{min}$ and $s^\\mathrm{max}$ (speed bounds), $\\tau^\\mathrm{min}$ and $\\tau^\\mathrm{max}$\n", "(waypoint arrival time bounds), and the fuel use function $\\Phi: \\mathbf{R} \\rightarrow \\mathbf{R}$. You are to choose the speeds\n", "$s_1, \\ldots, s_n$ so as to minimize the total fuel consumed in kg." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", @@ -618,7 +621,6 @@ }, "outputs": [], "source": [ - "import cvxpy as cp\n", "\n", "# TODO: your code here\n", "# store the optimal speeds in s\n" @@ -660,9 +662,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/DCP_analysis.ipynb b/book/docs/exercises/notebooks/DCP_analysis.ipynb index b782b164..d693c4c1 100644 --- a/book/docs/exercises/notebooks/DCP_analysis.ipynb +++ b/book/docs/exercises/notebooks/DCP_analysis.ipynb @@ -13,41 +13,50 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "In this exercise, you will fix optimization problems that break the DCP rules by identifying the DCP error and then rewriting the problem." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, - "outputs": [], - "source": [ - "import cvxpy as cp\n", - "import numpy as np" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [ + "import cvxpy as cp\n", + "import numpy as np" + ] }, { "cell_type": "markdown", - "source": [ - "## Problem 1.\n", - "\n", - "$\\min\\{ \\sqrt{x^2 + 1 } : x \\in \\mathbb{R} \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Problem 1.\n", + "\n", + "$\\min\\{ \\sqrt{x^2 + 1 } : x \\in \\mathbb{R} \\}$" + ] }, { "cell_type": "code", @@ -69,17 +78,20 @@ }, { "cell_type": "markdown", - "source": [ - "## Problem 2.\n", - "\n", - "$\\min\\{x + 2 \\,:\\, 5 = 2 / x,~~ x > 0 \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Problem 2.\n", + "\n", + "$\\min\\{x + 2 \\,:\\, 5 = 2 / x,~~ x > 0 \\}$" + ] }, { "cell_type": "code", @@ -100,16 +112,19 @@ }, { "cell_type": "markdown", - "source": [ - "## Problem 3.\n", - "$\\min\\{ x + 2 \\,:\\, 5 \\leq 2 / x^2,~~ x \\in \\mathbb{R} \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Problem 3.\n", + "$\\min\\{ x + 2 \\,:\\, 5 \\leq 2 / x^2,~~ x \\in \\mathbb{R} \\}$" + ] }, { "cell_type": "code", @@ -130,16 +145,19 @@ }, { "cell_type": "markdown", - "source": [ - "## Problem 4.\n", - "$\\min\\{ 1/ x \\,:\\, 0 \\leq x^2 / y, ~~ y \\geq 1, ~~ x > 0 \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Problem 4.\n", + "$\\min\\{ 1/ x \\,:\\, 0 \\leq x^2 / y, ~~ y \\geq 1, ~~ x > 0 \\}$" + ] }, { "cell_type": "code", @@ -161,16 +179,19 @@ }, { "cell_type": "markdown", - "source": [ - "## Problem 5.\n", - "$\\min\\{ x + 2 \\,:\\, \\exp(2x) + \\exp(3x) \\leq \\exp(5x) \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Problem 5.\n", + "$\\min\\{ x + 2 \\,:\\, \\exp(2x) + \\exp(3x) \\leq \\exp(5x) \\}$" + ] }, { "cell_type": "code", @@ -191,16 +212,19 @@ }, { "cell_type": "markdown", - "source": [ - "## Bonus Problem 1.\n", - "$\\min\\{ -(\\max\\{x, 4\\} - 3)^2 \\,:\\, x \\geq 1 \\}$" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "## Bonus Problem 1.\n", + "$\\min\\{ -(\\max\\{x, 4\\} - 3)^2 \\,:\\, x \\geq 1 \\}$" + ] }, { "cell_type": "code", @@ -221,19 +245,22 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%% md\n" + } + }, "source": [ "## Bonus Problem 2.\n", "\n", "$\\min\\left\\{ \\sum_{i=1}^m c_i \\frac{x_i}{u_i - x_i} \\,:\\, ~ u > x,~~ x \\in \\mathbb{R}^m \\right\\}$\n", "\n", "where $c$ and $u$ are nonnegative vectors." - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - } + ] }, { "cell_type": "code", @@ -279,9 +306,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/Lasso.ipynb b/book/docs/exercises/notebooks/Lasso.ipynb index 6421d730..733eee83 100644 --- a/book/docs/exercises/notebooks/Lasso.ipynb +++ b/book/docs/exercises/notebooks/Lasso.ipynb @@ -2,63 +2,84 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# LASSO" - ], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%% md\n" } - } + }, + "source": [ + "# LASSO" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "We wish to recover a sparse vector $x \\in \\mathbf{R}^n$ from measurements $y \\in \\mathbf{R}^m$. Our measurement model tells us that" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "$$\n", "y = Ax + v,\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "where $A \\in \\mathbf{R}^{m \\times n}$ is a known matrix and $v \\in \\mathbf{R}^m$ is unknown measurement error.\n", "For our demonstration the entries of $v$ are sampled from the normal distribution with mean zero and\n", "standard deviation $\\sigma$ (by default, $\\sigma = 1$).\n", "\n", "We can first try to recover $x$ by solving the optimization problem" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "$$\n", "\\begin{array}{ll} \\mbox{minimize} & ||Ax - y||^2_2 + \\gamma ||x||^2_2.\\\\\n", "\\end{array}\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "This problem is called ridge regression.\n", "\n", @@ -66,35 +87,47 @@ "values of $\\gamma$. Use the plotting code to compare the estimated $x$ with the true $x$.\n", "\n", "A more effective approach is to solve the LASSO problem" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "$$\n", "\\begin{array}{ll} \\mbox{minimize} & ||Ax - y||^2_2 + \\gamma \\|x\\|_1.\\\\\n", "\\end{array}\n", "$$" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "How many measurements $m$ are needed to find an accurate $x$ with ridge regression? How about with the LASSO?" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "# Ridge regression vs. LASSO to estimate sparse x.\n", @@ -116,19 +149,24 @@ "\n", "x = cp.Variable(n)\n", "gamma = None # set me! Initialize to 1." - ], + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, + }, "outputs": [], "source": [ + "import matplotlib.pyplot as plt\n", + "\n", "ridge_loss = None # set me\n", "ridge = cp.Problem(cp.Minimize(ridge_loss))\n", "ridge.solve(solver='ECOS')\n", @@ -139,23 +177,25 @@ "lasso.solve(solver='ECOS')\n", "x_lasso = x.value\n", "\n", - "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "plt.semilogy(range(n), np.sort(np.abs(true_x - x_ridge)), label=\"ridge errors\")\n", "plt.semilogy(range(n), np.sort(np.abs(true_x - x_lasso)), label=\"lasso errors\")\n", "plt.legend()\n", "plt.show()" - ], + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, + }, "outputs": [], "source": [ "# Managing parallelism for cross-validation.\n", @@ -182,17 +222,20 @@ "xs_dask = dask.compute(*dasklist, scheduler='processes')\n", "toc = time.time()\n", "t_dask = toc - tic\n" - ], + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": null, + }, "outputs": [], "source": [ "print(f'Time using a native loop \\n\\t{t_loop}')\n", @@ -204,25 +247,22 @@ "sol_errors = [la.norm(xs_loop[i] - true_x) for i in range(num_gamma)]\n", "plt.plot(sol_errors)\n", "plt.show()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "code", "execution_count": null, - "outputs": [], - "source": [], "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } + }, + "outputs": [], + "source": [] }, { "cell_type": "code", @@ -252,9 +292,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/exercises/notebooks/sum_k_largest.ipynb b/book/docs/exercises/notebooks/sum_k_largest.ipynb index 540138b2..a6d80edb 100644 --- a/book/docs/exercises/notebooks/sum_k_largest.ipynb +++ b/book/docs/exercises/notebooks/sum_k_largest.ipynb @@ -13,6 +13,9 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "Let $f_k(x)$ be the sum of the $k$ largest entries of $|x|$.\n", "\n", @@ -27,10 +30,7 @@ "For a wide $m \\times n$ matrix $A$, an $m$-vector $y$, and a tuning parameter $\\gamma$.\n", "\n", "The larger values of $x$ are \"more penalized\" as $\\gamma$ gets smaller." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", @@ -43,7 +43,6 @@ "outputs": [], "source": [ "import cvxpy as cp\n", - "import itertools\n", "\n", "def sum_abs_largest(expr, k):\n", " # implement me!\n", @@ -62,7 +61,6 @@ "source": [ "import numpy as np\n", "import scipy.linalg as la\n", - "import scipy.sparse as sp\n", "import time\n", "\n", "def make_problem_data(n, m, true_k):\n", diff --git a/book/docs/python_intro/notebooks/namespace.ipynb b/book/docs/python_intro/notebooks/namespace.ipynb index fcca05be..33bcf57c 100644 --- a/book/docs/python_intro/notebooks/namespace.ipynb +++ b/book/docs/python_intro/notebooks/namespace.ipynb @@ -13,12 +13,15 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "This notebook covers the basics of Python namespaces." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", @@ -48,6 +51,15 @@ { "cell_type": "code", "execution_count": 2, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [ { "name": "stdout", @@ -63,17 +75,20 @@ "\n", "# No need to use 'math.' prefix for individually imported functions\n", "print(cos(math.pi / 3))" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 3, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 3, + }, "outputs": [ { "name": "stdout", @@ -95,17 +110,20 @@ "for i in range(5):\n", " # print a random number in [0, 1)\n", " print(rn.random())" - ], + ] + }, + { + "cell_type": "code", + "execution_count": 4, "metadata": { "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, "pycharm": { "name": "#%%\n" } - } - }, - { - "cell_type": "code", - "execution_count": 4, + }, "outputs": [ { "name": "stdout", @@ -116,23 +134,18 @@ } ], "source": [ + "# flake8: noqa: F405, F403\n", "# Import all the 'math' functions into the namespace\n", "# Importing everything without a namespace is generally discouraged\n", "from math import *\n", "\n", "print(sin(pi))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -146,9 +159,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/book/docs/python_intro/notebooks/numpy_example.ipynb b/book/docs/python_intro/notebooks/numpy_example.ipynb index 1a535a68..315e20cb 100644 --- a/book/docs/python_intro/notebooks/numpy_example.ipynb +++ b/book/docs/python_intro/notebooks/numpy_example.ipynb @@ -13,12 +13,15 @@ }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "This notebook covers the basics of NumPy, the Python matrix library." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", @@ -89,6 +92,7 @@ "B = np.ones((3, 4))\n", "print(B)\n", "\n", + "# flake8: noqa: E741\n", "# Identity matrix\n", "I = np.eye(5)\n", "print(I)\n", @@ -241,7 +245,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -255,9 +259,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.12.7" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 }