{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import manganite\n", "%load_ext manganite" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Portfolio Selection Optimization\n", "This model is an example of the classic [Markowitz portfolio selection optimization model](https://en.wikipedia.org/wiki/Markowitz_model). We want to find the fraction of the portfolio to invest among a set of stocks that balances risk and return. It is a Quadratic Programming (QP) model with vector and matrix data for returns and risk, respectively. This is best suited to a matrix formulation, so we use the Gurobi Python *matrix* interface. The basic model is fairly simple, so we also solve it parametrically to find the efficient frontier.\n", "\n", "**Download the Repository**
\n", "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n", "\n", "**Gurobi License**
\n", "In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-MUI-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-EDU-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as an *academic user*.\n", "\n", "\n", "## Model Formulation\n", "### Parameters\n", "\n", "We use the [Greek values](https://en.wikipedia.org/wiki/Greeks_\\(finance\\)) that are traditional in finance:\n", "\n", "- $$ \\delta $$: n-element vector measuring the change in price for each stock\n", "- $$ \\sigma $$: n x n matrix measuring the covariance among stocks\n", "\n", "There is one additional parameter when solving the model parametrically:\n", "\n", "- r: target return\n", "\n", "\n", "### Decision Variables\n", "- $$ x \\ge 0 $$ : n-element vector where each element represents the fraction of the porfolio to invest in each stock\n", "\n", "### Objective Function\n", "Minimize the total risk, a convex quadratic function:\n", "
\n",
    "$$\n",
    "\\begin{equation}\n",
    "\\min x^t \\cdot \\sigma \\cdot x\n",
    "\\end{equation}\n",
    "$$\n",
    "
\n", "\n", "### Constraints\n", "\n", "Allocate the entire portfolio: the total investments should be 1.0 (100%), where $e$ is a unit vector (all 1's):\n", "
\n",
    "$$\n",
    "\\begin{equation}\n",
    "e \\cdot x = 1\n",
    "\\end{equation}\n",
    "$$\n",
    "
\n", "Return: When we solve the model parametrically for different return values $r$, we add a constraint on the target return:\n", "
\n",
    "$$\n",
    "\\begin{equation}\n",
    "\\delta \\cdot x = r\n",
    "\\end{equation}\n",
    "$$\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Python Implementation\n", "### Stock data\n", "Use [yfinance](https://pypi.org/project/yfinance/) library to get the latest 2 years of _actual stock data_ from the 20 most profitable US companies, [according to Wikipedia in April 2021](https://en.wikipedia.org/wiki/List_of_largest_companies_in_the_United_States_by_revenue#List_of_companies_by_profit).\n", "### Dashboard\n", "Use manganite package to create a beautiful dashoard from the jupyter notebook" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "import yfinance as yf\n", "import numpy as np\n", "\n", "import gurobipy as gp\n", "from gurobipy import GRB\n", "from math import sqrt\n", "\n", "import pandas as pd\n", "import plotly.express as px\n", "import plotly.graph_objects as go\n", "\n", "import plotly.io as pio\n", "pio.templates.default = 'plotly_white'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%mnn widget --type text --tab \"Stock Selector\" --header \"Select your stocks\" --var stocks --position 0 0 3\n", "stocks = 'AAPL, MSFT, JPM, GOOG, BAC, INTC, WFC, Meta'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Split the input string into a list using the column separator\n", "tick_marks_list = stocks.split(',')\n", "\n", "# Remove unwanted characters and spaces from each element\n", "clean_tick_marks = [tick.strip() for tick in tick_marks_list]\n", "\n", "data = yf.download(clean_tick_marks, period='2y')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%mnn widget --type plot --var fig_stocks --tab \"Stock Selector\" --position 1 0 4 --header \"Stock prices\"\n", "\n", "df_closing = data['Close']\n", "fig_stocks = px.line(df_closing, x=df_closing.index, y=df_closing.columns,\n", " # hover_data={\"date\": \"|%B %d, %Y\"},\n", " )\n", "\n", "# Update the layout to customize axis labels\n", "fig_stocks.update_yaxes(title_text='Stock Price ($)')\n", "\n", "fig_stocks.update_xaxes(\n", " title_text='',\n", " dtick=\"M1\",\n", " tickformat=\"%b\\n%Y\",\n", " ticklabelmode=\"period\")\n", "\n", "fig_stocks.update_layout(legend_title=\"Stock\",)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%mnn execute --on button \"Optimize Portfolio\" --returns solution\n", "\n", "#calculating greeks\n", "closes = np.transpose(np.array(data.Close)) # matrix of daily closing prices\n", "absdiff = np.diff(closes) # change in closing price each day\n", "reldiff = np.divide(absdiff, closes[:,:-1]) # relative change in daily closing price\n", "delta = np.mean(reldiff, axis=1) # mean price change\n", "sigma = np.cov(reldiff) # covariance (standard deviations)\n", "std = np.std(reldiff, axis=1) # standard deviation\n", "df_plot = pd.DataFrame({'std': std, 'delta': delta, 'stocks':clean_tick_marks})\n", "print('solving QP model')\n", "# Create an empty model\n", "m = gp.Model('portfolio')\n", "\n", "# Add matrix variable for the stocks\n", "x = m.addMVar(len(clean_tick_marks))\n", "\n", "# Objective is to minimize risk (squared). This is modeled using the\n", "# covariance matrix, which measures the historical correlation between stocks\n", "portfolio_risk = x @ sigma @ x\n", "m.setObjective(portfolio_risk, GRB.MINIMIZE)\n", "\n", "# Fix budget with a constraint\n", "m.addConstr(x.sum() == 1, 'budget')\n", "\n", "# Verify model formulation\n", "# m.write('portfolio_selection_optimization.lp')\n", "\n", "# Optimize model to find the minimum risk portfolio\n", "m.optimize()\n", "\n", "minrisk_volatility = sqrt(m.ObjVal)\n", "minrisk_return = delta @ x.X\n", "\n", "# Create an expression representing the expected return for the portfolio\n", "portfolio_return = delta @ x\n", "\n", "target = m.addConstr(portfolio_return == minrisk_return, 'target')\n", "\n", "# solution = pd.DataFrame(data=np.append(x.X, [minrisk_volatility, minrisk_return]),\n", "# index=clean_tick_marks + ['Volatility', 'Expected Return'],\n", "# columns=['Minimum Risk Portfolio'])\n", "\n", "solution = pd.DataFrame(data=x.X,\n", " index=clean_tick_marks,\n", " columns=['Minimum Risk Portfolio'])\n", "\n", "exp_return = minrisk_return\n", "exp_volatility = minrisk_volatility\n", "\n", "# Solve for efficient frontier by varying target return\n", "frontier = np.empty((2,0))\n", "for r in np.linspace(delta.min(), delta.max(), 25):\n", " target.rhs = r\n", " m.optimize()\n", " frontier = np.append(frontier, [[sqrt(m.ObjVal)],[r]], axis=1)\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%mnn widget --type plot --var fig_bar --tab \"Portfolio\" --position 0 0 3 --header \"Minimum Risk Portfolio\"\n", "# Create a pie chart\n", "fig_pie = px.pie(solution, values='Minimum Risk Portfolio', names=solution.index,\n", " title=f'Your Portfolio Distribution
Expected Return: {round(exp_return,6)}
Volatility: {round(exp_volatility,4)}')\n", "fig_pie.update_traces(textposition='inside', textinfo='percent+label')\n", "\n", "# Create a bar chart\n", "fig_bar = px.bar(solution, x=solution.index, y='Minimum Risk Portfolio',\n", "title=f'Your Portfolio Distribution
Expected Return: {round(exp_return,6)}
Volatility: {round(exp_volatility,4)}',\n", ")\n", "\n", "# Hide x-axis and y-axis labels\n", "fig_bar.update_xaxes(title_text='')\n", "fig_bar.update_yaxes(title_text='')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%mnn widget --type plot --var fig --tab \"Portfolio\" --position 0 3 3 --header \"Efficient Frontier\"\n", "\n", "update = solution\n", "# Plot volatility versus expected return for individual stocks\n", "fig1 = px.scatter(df_plot, x=\"std\", y=\"delta\" ,\n", " labels = { \"std\": \"Volatility (standard deviation)\", \"delta\": \"Expected Return\"}, text=\"stocks\" )\n", "fig1.update_traces(textposition=\"bottom right\")\n", "\n", "# Plot volatility versus expected return for minimum risk portfolio\n", "\n", "fig2 = px.scatter(x=[minrisk_volatility], y=[minrisk_return],text = [\"Minimum Risk
Portfolio\"])\n", "fig2.update_traces(textposition=\"bottom right\")\n", "fig = go.Figure(data=fig1.data + fig2.data)\n", "\n", "# Plot efficient frontier\n", "\n", "fig.add_trace(go.Scatter(x=frontier[0], y=frontier[1], mode='lines', name='Efficient Frontier'))\n", "\n", "# Set x and y labels using update_xaxes and update_yaxes\n", "fig.update_xaxes(title_text=\"Volatility (standard deviation)\")\n", "fig.update_yaxes(title_text=\"Expected Return\")\n", "\n", "fig.update_layout(legend=dict(\n", " yanchor=\"bottom\",\n", " y=0.01,\n", " xanchor=\"right\",\n", " x=0.99\n", "))\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.4" } }, "nbformat": 4, "nbformat_minor": 4 }