{ "cells": [ { "cell_type": "markdown", "id": "aa28fe02", "metadata": {}, "source": [ "# Rebalancing with Transaction Costs\n", "\n", "The *standard mean-variance (Markowitz) portfolio selection model* determines an optimal investment portfolio that balances risk and expected return. Please refer to the [annotated list of references](../literature.rst#portfolio-optimization) for more background information on portfolio optimization.\n", "\n", "In this notebook, we rebalance an existing portfolio to maximize the expected return of the portfolio while constraining the admissible variance (risk) to a given maximum level. Additionally, we account for *transaction costs* and *fees*:\n", "\n", "* The fixed transaction costs occur for each transaction and are independent of the amount that is bought or sold.\n", "* The variable transaction fees are proportional to the traded amount." ] }, { "cell_type": "code", "execution_count": 1, "id": "840d211c", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:50.466902Z", "iopub.status.busy": "2025-01-31T10:05:50.466656Z", "iopub.status.idle": "2025-01-31T10:05:51.247307Z", "shell.execute_reply": "2025-01-31T10:05:51.246557Z" }, "nbsphinx": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: numpy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (2.2.2)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: scipy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (1.15.1)\r\n", "Requirement already satisfied: gurobipy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (11.0.3)\r\n", "Requirement already satisfied: pandas in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (2.2.3)\r\n", "Requirement already satisfied: matplotlib in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (3.10.0)\r\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2.9.0.post0)\r\n", "Requirement already satisfied: pytz>=2020.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2025.1)\r\n", "Requirement already satisfied: tzdata>=2022.7 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2025.1)\r\n", "Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (1.3.1)\r\n", "Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (0.12.1)\r\n", "Requirement already satisfied: fonttools>=4.22.0 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (4.55.8)\r\n", "Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (1.4.8)\r\n", "Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (24.2)\r\n", "Requirement already satisfied: pillow>=8 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (11.1.0)\r\n", "Requirement already satisfied: pyparsing>=2.3.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (3.2.1)\r\n", "Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "# Install dependencies\n", "%pip install numpy scipy gurobipy pandas matplotlib" ] }, { "cell_type": "code", "execution_count": 2, "id": "59966aa7", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.249456Z", "iopub.status.busy": "2025-01-31T10:05:51.249254Z", "iopub.status.idle": "2025-01-31T10:05:51.871065Z", "shell.execute_reply": "2025-01-31T10:05:51.870392Z" } }, "outputs": [], "source": [ "import gurobipy as gp\n", "import pandas as pd\n", "import numpy as np\n", "import random\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 3, "id": "bf8380d2", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.873215Z", "iopub.status.busy": "2025-01-31T10:05:51.872933Z", "iopub.status.idle": "2025-01-31T10:05:51.881291Z", "shell.execute_reply": "2025-01-31T10:05:51.880724Z" }, "nbsphinx": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Set parameter WLSAccessID\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Set parameter WLSSecret\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Set parameter LicenseID to value 2443533\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "WLS license 2443533 - registered to Gurobi GmbH\n" ] } ], "source": [ "# Hidden cell to avoid licensing messages\n", "# when docs are generated.\n", "with gp.Model():\n", " pass" ] }, { "cell_type": "markdown", "id": "c9f658c5", "metadata": {}, "source": [ "## Input Data\n", "\n", "The following input data is used within the model:\n", "\n", "- $S$: set of stocks\n", "- $\\mu$: vector of expected returns\n", "- $\\Sigma$: PSD variance-covariance matrix\n", " - $\\sigma_{ij}$ covariance between returns of assets $i$ and $j$\n", " - $\\sigma_{ii}$ variance of return of asset $i$" ] }, { "cell_type": "code", "execution_count": 4, "id": "740cd737", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.883316Z", "iopub.status.busy": "2025-01-31T10:05:51.882966Z", "iopub.status.idle": "2025-01-31T10:05:51.887792Z", "shell.execute_reply": "2025-01-31T10:05:51.887204Z" } }, "outputs": [], "source": [ "# Import some example data set\n", "Sigma = pd.read_pickle(\"sigma.pkl\")\n", "mu = pd.read_pickle(\"mu.pkl\")" ] }, { "cell_type": "markdown", "id": "a357031c", "metadata": {}, "source": [ "## Formulation\n", "\n", "The model rebalances an existing portfolio. It maximizes the expected return of the new portfolio, constraining the variance of the portfolio not to exceed a specified level, and also accounts for fixed and variable transaction costs and fees.\n", "Mathematically, this results in a convex quadratically constrained mixed-integer optimization problem.\n", "\n", "### Model parameters\n", "\n", "The following parameters are used within the model:\n", "\n", "- $\\bar\\sigma^2$: maximal admissible variance for the portfolio return\n", "- $x^0_i$: holdings of asset $i$ in the initial portfolio.\n", "Usually, the starting portfolio is given by the previous trading period.\n", "In this notebook, we choose a simple starting portfolio by equally distributing the capital among the assets with the largest returns.\n", "\n", "- $\\ell>0$: lower bound on transaction size\n", "\n", "- $c_i^+, c_i^-$: fixed transaction cost when buying/selling stock $i$\n", "- $f_i^+, f_i^-$: variable transaction cost for buying/selling stock $i$, relative to total investment value" ] }, { "cell_type": "code", "execution_count": 5, "id": "bfbf5afe", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.889581Z", "iopub.status.busy": "2025-01-31T10:05:51.889409Z", "iopub.status.idle": "2025-01-31T10:05:51.895434Z", "shell.execute_reply": "2025-01-31T10:05:51.894879Z" } }, "outputs": [], "source": [ "# Values for the model parameters:\n", "V = 4.0 # Maximal admissible variance (sigma^2)\n", "l = 0.001 # Minimal transaction size\n", "\n", "# Fixed transaction costs\n", "c_plus = 0.00002 * np.ones(mu.shape)\n", "c_minus = 0.00002 * np.ones(mu.shape)\n", "# Variable transaction fees\n", "f_plus = 0.0002 * np.ones(mu.shape)\n", "f_minus = 0.0002 * np.ones(mu.shape)\n", "\n", "# Initial portfolio: Equally distributed among largest return assets\n", "x0 = pd.Series(index=mu.index, data=np.zeros(mu.shape))\n", "x0.loc[mu.nlargest(20).index] = 1.0 / 20.0" ] }, { "cell_type": "markdown", "id": "37eeb64d", "metadata": {}, "source": [ "### Decision Variables\n", "We need several sets of decision variables:\n", "\n", "1. The proportions of capital invested in the rebalanced portfolio among the considered stocks. The corresponding vector of positions is denoted by $x$ with its component $x_i$ denoting the proportion of capital invested in stock $i$.\n", "\n", "The other sets of variables distinguish between buys and sells:\n", "\n", "2. The proportions of each stock *bought* and included in the rebalanced portfolio. The corresponding vector of positions is denoted by $x^+$ with its component $x^+_i$ denoting the proportion of capital representing the purchase of stock $i$.\n", "\n", "3. The proportions of each stock *sold* from the existing portfolio. The corresponding vector of sales is denoted by $x^-$ with its component $x^-_i$ denoting the proportion of capital representing the sale of stock $i$.\n", "\n", "4. Binary variables $b_i^+$ indicating whether or not asset $i$ is *bought*. If $b_i^+$ is 0, asset $i$ is not bought, that is $x_i \\leq x^0_i$. Otherwise, if $b_i^+$ is 1, the investor buys some amount (at least $\\ell$) of asset $i$ and $x_i \\geq x^0_i + \\ell$.\n", "\n", "5. Binary variables $b_i^-$ indicating whether or not asset $i$ is *sold*. If $b_i^-$ is 0, asset $i$ is not sold, that is $x_i \\geq x^0_i$. Otherwise, if $b_i^-$ is 1, the investor sells some amount (at least $\\ell$) of asset $i$ and $x_i \\leq x^0_i - \\ell$.\n", "\n", "### Variable Bounds\n", "\n", "Each position must be between 0 and 1; this prevents leverage and short-selling:\n", "\n", "$$0\\leq x_i\\leq 1 \\; , \\; i \\in S$$\n", "\n", "The buy and sell proportions must be non-negative:\n", "\n", "$$ x_i^+, x_i^- \\geq 0\\; , \\, i \\in S$$\n", "\n", "Variables $b_i^+, b_i^-$ are binary:\n", "\n", "$$b_i^+, b_i^- \\in \\{0,1\\} \\; , \\; i \\in S$$" ] }, { "cell_type": "code", "execution_count": 6, "id": "ada9a9db", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.897224Z", "iopub.status.busy": "2025-01-31T10:05:51.897035Z", "iopub.status.idle": "2025-01-31T10:05:51.903850Z", "shell.execute_reply": "2025-01-31T10:05:51.903270Z" } }, "outputs": [], "source": [ "# Create an empty optimization model\n", "m = gp.Model()\n", "\n", "# Add variables: x[i] denotes the proportion invested in stock i\n", "x = m.addMVar(len(mu), lb=0, ub=1, name=\"x\")\n", "# Add variables: x_plus[i] denotes the proportion of stock i bought\n", "x_plus = m.addMVar(len(mu), lb=0, ub=1, name=\"x_plus\")\n", "# Add variables: x_minus[i] denotes the proportion of stock i sold\n", "x_minus = m.addMVar(len(mu), lb=0, ub=1, name=\"x_minus\")\n", "# Add variables: b_plus[i]=1 if stock i is bought, and b_plus[i]=0 otherwise\n", "b_plus = m.addMVar(len(mu), vtype=gp.GRB.BINARY, name=\"b_plus\")\n", "# Add variables: b_minus[i]=1 if stock i is sold, and b_minus[i]=0 otherwise\n", "b_minus = m.addMVar(len(mu), vtype=gp.GRB.BINARY, name=\"b_minus\")" ] }, { "cell_type": "markdown", "id": "55bedc38", "metadata": {}, "source": [ "### Constraints\n", "\n", "The final proportion of capital invested in a stock in the rebalanced portfolio is obtained by taking into account the buys and sells of this stock as well as the initial holdings in the existing portfolio:\n", "\n", "\\begin{equation*}\n", "x_i = x^0_i + x^+_i - x^-_i \\; , \\; i \\in S \\tag{1}\n", "\\end{equation*}\n", "\n", "The estimated risk must not exceed a prespecified maximal admissible level of variance $\\bar\\sigma^2$:\n", "\n", "$$x^\\top \\Sigma x \\leq \\bar\\sigma^2$$" ] }, { "cell_type": "code", "execution_count": 7, "id": "55f03b91", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:51.905664Z", "iopub.status.busy": "2025-01-31T10:05:51.905492Z", "iopub.status.idle": "2025-01-31T10:05:52.079957Z", "shell.execute_reply": "2025-01-31T10:05:52.079380Z" } }, "outputs": [], "source": [ "%%capture\n", "# Proportion Changes balance constraint; see formula (1) above\n", "m.addConstr(x == x0.to_numpy() + x_plus - x_minus, name=\"Position_Balance\")\n", "\n", "# Upper bound on variance\n", "m.addConstr(x @ Sigma.to_numpy() @ x <= V, name=\"Variance\")" ] }, { "cell_type": "markdown", "id": "f224b85f", "metadata": {}, "source": [ "Note that in an optimal solution, at most one of the two variables $x^+_i$ and $x^-_i$ should take a nonzero value for each $i$.\n", "\n", "To enforce the minimal transaction size, we use the binary variables $b^+,b^-$ and the following sets of discrete constraints, which will enforce:\n", "\n", "\\begin{equation*}\n", "x_i^+ \\geq \\ell\\ \\text{ if } b_i^+=1 \\ \\text{ and } x_i^+ = 0 \\text{ otherwise} \\\\\n", "\\end{equation*}\n", "\\begin{equation*}\n", "x_i^- \\geq \\ell\\ \\text{ if } b_i^-=1 \\ \\text{ and } x_i^- = 0 \\text{ otherwise}\n", "\\end{equation*}\n", "\n", "\n", "Ensure that $x_i^+ = 0$ if $b_i^+ = 0$:\n", "\n", "\\begin{equation*}\n", "x_i^+ \\leq b_i^+ \\; , \\; i \\in S\\tag{2}\n", "\\end{equation*}\n", "\n", "Note that we are using that $x_i^+$ has an upper bound of 1 because we cannot buy or sell more than the total capital in one position. Hence, if $b_i^+ = 1$, the above constraint is non-restrictive.\n", "\n", "\n", "Ensure a minimal transaction size of $\\ell$ if asset $i$ is traded:\n", "\n", "\\begin{equation*}\n", "x_i^+ \\geq \\ell b_i^+ \\; , \\; i \\in S\\tag{3}\n", "\\end{equation*}\n", "\n", "Hence $b_i^+ = 1$ implies $x_i^+ \\geq \\ell$. If $b_i^+ = 0$, this constraint is non-restrictive since $x_i^+$ has a lower bound of 0.\n", "\n", "Analogously, we define similar constraints for $x_i^-$ and $b_i^-$:\n", "\n", "$$ x_i^- \\leq b_i^- \\; , \\; i \\in S$$\n", "$$ x_i^- \\geq \\ell b_i^- \\; , \\; i \\in S$$\n", "\n", "\n", "Finally, we require that the same asset is not bought and sold at the same time:\n", "\n", "$$\n", "b^+_i + b^-_i \\leq 1 \\; , \\; i \\in S\n", "$$" ] }, { "cell_type": "code", "execution_count": 8, "id": "ce8f0a3b", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:52.082603Z", "iopub.status.busy": "2025-01-31T10:05:52.082282Z", "iopub.status.idle": "2025-01-31T10:05:52.099763Z", "shell.execute_reply": "2025-01-31T10:05:52.099158Z" } }, "outputs": [], "source": [ "%%capture\n", "# Force x_plus, x_minus to 0 if not traded; see formula (2) above\n", "m.addConstr(x_plus <= b_plus, name=\"Indicator_Buy\")\n", "m.addConstr(x_minus <= b_minus, name=\"Indicator_Sell\")\n", "# Minimal buy/sell; see formula (3) above\n", "m.addConstr(x_plus >= l * b_plus, name=\"Minimal_Buy\")\n", "m.addConstr(x_minus >= l * b_minus, name=\"Minimal_Sell\")\n", "\n", "# Constraint preventing the simultaneous buy and sell of the same security\n", "m.addConstr(b_plus + b_minus <= 1, name=\"Mutual_Exclusivity\")" ] }, { "cell_type": "markdown", "id": "1c3fe6ca", "metadata": {}, "source": [ "Without any costs and fees, we would have the budget constraint $\\sum_{i} x_i = 1$ to ensure that all investments sum up to one.\n", "However, we must also pay the resulting fixed costs and variable transaction fees from the capital.\n", "\n", "The *fixed costs* occur once per transaction (purchase or sale) and do not depend on the transaction size; hence the fixed costs for asset $i$ are $c_i^+b_i^+ + c_i^-b_i^-$. The *variable fees* are proportional to the transaction size; hence the fees for asset $i$ are $f_i^+x_i^+ + f_i^-x_i^-$.\n", "\n", "We include these in the the left-hand side of the equation:\n", "\n", "\\begin{equation*}\n", "\\underbrace{\\sum_{i \\in S}x_i}_\\text{investments}\n", "+ \\underbrace{\\sum_{i \\in S}c_i^+b_i^+\n", "+ \\sum_{i\\in S}c_i^-b_i^-}_\\text{fixed costs for buying and selling}\n", "+ \\underbrace{\\sum_{i \\in S}f_i^+x_i^+\n", "+ \\sum_{i \\in S}f_i^-x_i^-}_\\text{variable fees for buying and selling}\n", "= 1 \\tag{4}\n", "\\end{equation*}" ] }, { "cell_type": "code", "execution_count": 9, "id": "74bbe839", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:52.102849Z", "iopub.status.busy": "2025-01-31T10:05:52.102172Z", "iopub.status.idle": "2025-01-31T10:05:52.108960Z", "shell.execute_reply": "2025-01-31T10:05:52.108395Z" } }, "outputs": [], "source": [ "# Budget constraint: all investments, costs, and fees sum up to 1, see formula (4) above\n", "budget_constr = m.addConstr(\n", " x.sum() + c_plus @ b_plus + c_minus @ b_minus + f_plus @ x_plus + f_minus @ x_minus\n", " == 1,\n", " name=\"Budget_Constraint\",\n", ")" ] }, { "cell_type": "markdown", "id": "0e63daed", "metadata": {}, "source": [ "### Objective Function\n", "The objective is to maximize the expected return of the portfolio:\n", "$$\\max_x \\mu^\\top x $$" ] }, { "cell_type": "code", "execution_count": 10, "id": "81f5da5f", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:52.110743Z", "iopub.status.busy": "2025-01-31T10:05:52.110576Z", "iopub.status.idle": "2025-01-31T10:05:52.113867Z", "shell.execute_reply": "2025-01-31T10:05:52.113267Z" } }, "outputs": [], "source": [ "# Define objective: Maximize expected return\n", "m.setObjective(mu.to_numpy() @ x, gp.GRB.MAXIMIZE)" ] }, { "cell_type": "markdown", "id": "eacdebc7", "metadata": {}, "source": [ "We now solve the optimization problem to provable optimality of 1%.\n", "By default, Gurobi uses a [MIP gap](https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#parametermipgap) of 1e-4 (=0.01%), but many real-world application do not need such a tight gap. Hence, we relax this to 1% to improve performance." ] }, { "cell_type": "code", "execution_count": 11, "id": "d6b0f5b5", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:05:52.115556Z", "iopub.status.busy": "2025-01-31T10:05:52.115386Z", "iopub.status.idle": "2025-01-31T10:06:05.496812Z", "shell.execute_reply": "2025-01-31T10:06:05.496186Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Set parameter MIPGap to value 0.01\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Gurobi Optimizer version 11.0.3 build v11.0.3rc0 (linux64 - \"Ubuntu 24.04.1 LTS\")\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "CPU model: AMD EPYC 7763 64-Core Processor, instruction set [SSE2|AVX|AVX2]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Thread count: 1 physical cores, 2 logical processors, using up to 2 threads\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "WLS license 2443533 - registered to Gurobi GmbH\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimize a model with 2773 rows, 2310 columns and 8316 nonzeros\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model fingerprint: 0xa5e8a251\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model has 1 quadratic constraint\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Variable types: 1386 continuous, 924 integer (924 binary)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Coefficient statistics:\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Matrix range [2e-05, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " QMatrix range [3e-03, 1e+02]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Objective range [7e-02, 6e-01]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Bounds range [1e+00, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " RHS range [5e-02, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " QRHS range [4e+00, 4e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolve removed 1788 rows and 1346 columns\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolve time: 0.13s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolved: 985 rows, 964 columns, 2972 nonzeros\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolved model has 1 quadratic constraint(s)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Variable types: 482 continuous, 482 integer (482 binary)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Root relaxation: objective 5.916849e-01, 39 iterations, 0.00 seconds (0.00 work units)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Nodes | Current Node | Objective Bounds | Work\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.59168 0 1 - 0.59168 - - 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.56263 0 2 - 0.56263 - - 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.43881 0 3 - 0.43881 - - 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.43876 0 4 - 0.43876 - - 1s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.42962 0 4 - 0.42962 - - 2s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.42883 0 5 - 0.42883 - - 2s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.41341 0 6 - 0.41341 - - 2s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.41340 0 6 - 0.41340 - - 3s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.40037 0 5 - 0.40037 - - 3s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.39495 0 5 - 0.39495 - - 3s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.39494 0 5 - 0.39494 - - 3s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.39260 0 6 - 0.39260 - - 3s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.39260 0 6 - 0.39260 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.39238 0 8 - 0.39238 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.38959 0 7 - 0.38959 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.38905 0 8 - 0.38905 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.38818 0 9 - 0.38818 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.38471 0 8 - 0.38471 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37920 0 6 - 0.37920 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37838 0 6 - 0.37838 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37776 0 7 - 0.37776 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37755 0 8 - 0.37755 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37675 0 8 - 0.37675 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37633 0 8 - 0.37633 - - 4s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 0 0.37633 0 8 - 0.37633 - - 5s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "H 0 0 0.3425836 0.37633 9.85% - 5s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "H 0 0 0.3494349 0.37633 7.70% - 6s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "H 0 0 0.3496292 0.37633 7.64% - 7s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 2 0.37633 0 8 0.34963 0.37633 7.64% - 7s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "H 27 27 0.3524726 0.37631 6.76% 4.7 8s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 104 104 0.35933 16 1 0.35247 0.37613 6.71% 5.6 10s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "H 277 216 0.3531515 0.36921 4.55% 7.3 13s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Explored 279 nodes (2382 simplex iterations) in 13.37 seconds (12.54 work units)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Thread count was 2 (of 2 available processors)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Solution count 5: 0.353151 0.352473 0.349629 ... 0.342584\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimal solution found (tolerance 1.00e-02)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Best objective 3.531514902694e-01, best bound 3.554402307144e-01, gap 0.6481%\n" ] } ], "source": [ "m.Params.MIPGap = 0.01\n", "m.optimize()" ] }, { "cell_type": "markdown", "id": "72a6be8b", "metadata": {}, "source": [ "Display basic solution data, costs, and fees. For clarity, we round all solution quantities to five digits." ] }, { "cell_type": "code", "execution_count": 12, "id": "49176bdc", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:06:05.498751Z", "iopub.status.busy": "2025-01-31T10:06:05.498526Z", "iopub.status.idle": "2025-01-31T10:06:05.514802Z", "shell.execute_reply": "2025-01-31T10:06:05.514206Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expected return: 0.353151\n", "Variance: 4.000000\n", "Solution time: 13.37 seconds\n", "\n", "Fixed costs: 0.000620\n", "Variable fees: 0.000331\n", "\n", "Number of positions: before 20, after 19\n", "Number of trades: 31.0 (13.0 buy(s), 18.0 sell(s))\n", "\n" ] }, { "data": { "text/html": [ "
\n", " | position | \n", "transaction | \n", "
---|---|---|
LLY | \n", "0.251369 | \n", "0.251369 | \n", "
PGR | \n", "0.130347 | \n", "0.130347 | \n", "
NVDA | \n", "0.110375 | \n", "0.060375 | \n", "
KDP | \n", "0.068958 | \n", "0.068958 | \n", "
AVGO | \n", "0.057072 | \n", "0.007072 | \n", "
NOC | \n", "0.054354 | \n", "0.054354 | \n", "
ORLY | \n", "0.051407 | \n", "0.051407 | \n", "
ODFL | \n", "0.041573 | \n", "0.041573 | \n", "
TMUS | \n", "0.039788 | \n", "0.039788 | \n", "
TTWO | \n", "0.037309 | \n", "0.037309 | \n", "
DPZ | \n", "0.034631 | \n", "0.034631 | \n", "
KR | \n", "0.030179 | \n", "0.030179 | \n", "
NFLX | \n", "0.022457 | \n", "-0.027543 | \n", "
UNH | \n", "0.019021 | \n", "0.019021 | \n", "
DXCM | \n", "0.018191 | \n", "-0.031809 | \n", "
TSLA | \n", "0.013521 | \n", "-0.036479 | \n", "
ENPH | \n", "0.010658 | \n", "-0.039342 | \n", "
MOH | \n", "0.007822 | \n", "-0.042178 | \n", "
FICO | \n", "0.000010 | \n", "-0.049990 | \n", "
CDNS | \n", "0.000008 | \n", "-0.049992 | \n", "
MU | \n", "0.000000 | \n", "-0.050000 | \n", "
AMD | \n", "0.000000 | \n", "-0.050000 | \n", "
NOW | \n", "0.000000 | \n", "-0.050000 | \n", "
KLAC | \n", "0.000000 | \n", "-0.050000 | \n", "
AMAT | \n", "0.000000 | \n", "-0.050000 | \n", "
MPWR | \n", "0.000000 | \n", "-0.050000 | \n", "
AXON | \n", "0.000000 | \n", "-0.050000 | \n", "
EPAM | \n", "0.000000 | \n", "-0.050000 | \n", "
FANG | \n", "0.000000 | \n", "-0.050000 | \n", "
PANW | \n", "0.000000 | \n", "-0.050000 | \n", "
LRCX | \n", "0.000000 | \n", "-0.050000 | \n", "