diff --git a/Pyomo_integration_example/README.md b/Pyomo_integration_example/README.md new file mode 100644 index 0000000..6446e33 --- /dev/null +++ b/Pyomo_integration_example/README.md @@ -0,0 +1,27 @@ +# Pyomo Integration Example + +This folder contains a Jupyter Notebook demonstrating how to integrate NVIDIA cuOpt as a solver backend for optimization problems modeled with Pyomo. + +## About Pyomo + +[Pyomo](https://www.pyomo.org/) is a Python-based open-source software package that supports a diverse set of optimization capabilities for formulating, solving, and analyzing optimization models. + +## Using cuOpt with Pyomo + +Pyomo supports cuOpt as a backend solver, allowing you to leverage GPU-accelerated optimization while using Pyomo's intuitive modeling syntax. This integration provides: + +- **Familiar API**: Use Pyomo's pythonic syntax for modeling +- **GPU Acceleration**: Benefit from cuOpt's high-performance GPU-based solving +- **Easy Solver Switching**: Compare different solvers by simply changing the solver parameter + +## Example Notebook + +### `p_median_problem.ipynb` + +This notebook demonstrates the classic p-median problem: +- **Problem**: Choosing facility locations to minimize the weighted distance while meeting assignment constraints. +- **Approach**: Model the problem using Pyomo and solve with cuOpt +- **Features**: + - Setting up decision variables and constraints with Pyomo + - Solving with setting `solver = pyo.SolverFactory("cuopt")` parameter + - Analyzing and visualizing results \ No newline at end of file diff --git a/Pyomo_integration_example/p_median_problem.ipynb b/Pyomo_integration_example/p_median_problem.ipynb new file mode 100644 index 0000000..f0df362 --- /dev/null +++ b/Pyomo_integration_example/p_median_problem.ipynb @@ -0,0 +1,429 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "fMaKbZo6Afgd" + }, + "source": [ + "# P-median Problem Example with Pyomo\n", + "\n", + "cuOpt is NVIDIA's GPU accelerated solver that delivers massive speedups for real-world LP, MIP, and VRP workloads.\n", + "\n", + "cuOpt seemlessly integrates with modeling languages. You can drop cuOpt into existing models built with pyomo, PuLP, cvxpy and AMPL, with minimal refactoring. Let's take a look at an example solving a simple MIP problem with cuOpt.\n", + "\n", + "To run this in Google Colab, download the notebook and upload it to Google Colab. Make sure you are running this on a T4 GPU.\n", + "\n", + "If you are running this in the cuOpt container, you are good to go!\n", + "\n", + "\n", + "## 1. Install Dependencies\n", + "\n", + "To make sure we are good to go, let's install Pyomo and cuOpt.\n", + "\n", + "__[Pyomo](https://github.com/Pyomo/pyomo)__ is a popular linear and mixed integer programming modeler written in Python.\n", + "\n", + "\n", + "If you are running this notebook in Google Colab, or elsewhere outside the container where cuOpt is not yet installed, uncomment the pip install command to install cuOpt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T2L7jTld2Qqj" + }, + "outputs": [], + "source": [ + "!pip install pyomo==6.10.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFLzH53z2Qoc" + }, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VeTiQIUJEQbR" + }, + "source": [ + "## 2. Problem Setup\n", + "\n", + "A city wants to place p = 6 ambulance stations to serve 50 demand zones (neighborhood centroids). Every zone must be assigned to exactly one open station, and we want to minimize total (weighted) travel distance.\n", + "\n", + "This is the classic p-median: choose facility locations + assign demand points to them.\n", + "\n", + "- We have N=50 demand points (clients)\n", + "\n", + "- M=20 candidate facility sites\n", + "\n", + "- Open exactly p=6 sites\n", + "\n", + "- Assign every client to one open site\n", + "\n", + "- Objective: Minimize total weighted distance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate problem parameters\n", + "\n", + "import random, math\n", + "random.seed(7)\n", + "\n", + "# sizes\n", + "N = 50 # clients\n", + "M = 20 # candidate sites\n", + "p = 6 # number of facilities to open\n", + "\n", + "clients = list(range(N))\n", + "sites = list(range(M))\n", + "\n", + "# coordinates\n", + "client_xy = {i: (random.uniform(0, 20), random.uniform(0, 20)) for i in clients}\n", + "site_xy = {j: (random.uniform(0, 20), random.uniform(0, 20)) for j in sites}\n", + "\n", + "# demand weights (e.g., expected calls)\n", + "w = {i: random.randint(1, 10) for i in clients}\n", + "\n", + "def euclid(a, b):\n", + " return math.hypot(a[0] - b[0], a[1] - b[1])\n", + "\n", + "# distance matrix d[i,j]\n", + "d = {(i, j): euclid(client_xy[i], site_xy[j]) for i in clients for j in sites}\n", + "\n", + "print(\"Example weights:\", list(w.items())[:5])\n", + "print(\"Example distance d[0,0]:\", d[(0,0)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Decision variables\n", + "\n", + "- yj ∈ {0,1} : open station at site j\n", + "\n", + "- xij ∈ {0,1} : assign client i to site j\n", + "\n", + "\n", + "### Objective\n", + "Minimize weighted distance: \n", + "\n", + "  min⁡ ∑i∈Ij∈J wij dij xij\n", + "\n", + "\n", + "### Constraints\n", + "1. Every client assigned to exactly one site:\n", + " ∑j xij = 1 ∀i\n", + " \n", + "2. Assign only to opened sites:\n", + " xij ≤ yj ∀i,j\n", + "\n", + "3. Open exactly p sites:\n", + " ∑j yj = p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Xw4x3_W14TU" + }, + "outputs": [], + "source": [ + "# Model the problem\n", + "import pyomo.environ as pyo\n", + "\n", + "m = pyo.ConcreteModel()\n", + "\n", + "# Sets\n", + "m.I = pyo.Set(initialize=clients) # clients\n", + "m.J = pyo.Set(initialize=sites) # sites\n", + "\n", + "# Params\n", + "m.w = pyo.Param(m.I, initialize=w, within=pyo.PositiveIntegers)\n", + "m.d = pyo.Param(m.I, m.J, initialize=d, within=pyo.NonNegativeReals)\n", + "m.p = pyo.Param(initialize=p)\n", + "\n", + "# Decision variables\n", + "m.x = pyo.Var(m.I, m.J, domain=pyo.Binary) # assignment\n", + "m.y = pyo.Var(m.J, domain=pyo.Binary) # open facility\n", + "\n", + "# Objective\n", + "def obj_rule(m):\n", + " return sum(m.w[i] * m.d[i, j] * m.x[i, j] for i in m.I for j in m.J)\n", + "m.obj = pyo.Objective(rule=obj_rule, sense=pyo.minimize)\n", + "\n", + "# Constraints\n", + "def assign_once_rule(m, i):\n", + " return sum(m.x[i, j] for j in m.J) == 1\n", + "m.assign_once = pyo.Constraint(m.I, rule=assign_once_rule)\n", + "\n", + "def open_link_rule(m, i, j):\n", + " return m.x[i, j] <= m.y[j]\n", + "m.open_link = pyo.Constraint(m.I, m.J, rule=open_link_rule)\n", + "\n", + "def open_p_rule(m):\n", + " return sum(m.y[j] for j in m.J) == m.p\n", + "m.open_p = pyo.Constraint(rule=open_p_rule)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OG02AqK2LpZ1" + }, + "source": [ + "## 3. Problem Solution\n", + "\n", + "Pyomo calls on the cuOpt solver, which finds the optimal site locations. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UL0TM5pTLp_m" + }, + "outputs": [], + "source": [ + "# Solve the problem using CUOPT\n", + "solver = pyo.SolverFactory(\"cuopt\")\n", + "results = solver.solve(m)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "# Print summary and plot results\n", + "\n", + "def print_summary(m):\n", + " print(\"\\n=============== Summary ===============\")\n", + " # opened sites\n", + " opened = [j for j in m.J if pyo.value(m.y[j]) > 0.5]\n", + " print(\"Opened sites:\", opened)\n", + " \n", + " # assignment per client\n", + " assign = {}\n", + " for i in m.I:\n", + " for j in m.J:\n", + " if pyo.value(m.x[i, j]) > 0.5:\n", + " assign[i] = j\n", + " break\n", + " \n", + " obj_val = pyo.value(m.obj)\n", + " total_weight = sum(w.values())\n", + " wavg_dist = sum(w[i] * d[(i, assign[i])] for i in clients) / total_weight\n", + " max_dist = max(d[(i, assign[i])] for i in clients)\n", + " \n", + " print(f\"Objective (weighted distance): {obj_val:.4f}\")\n", + " print(f\"Weighted avg distance: {wavg_dist:.4f}\")\n", + " print(f\"Max distance: {max_dist:.4f}\")\n", + " return opened, assign\n", + "\n", + "def plot_result(opened, assign):\n", + " import matplotlib.pyplot as plt\n", + " \n", + " plt.figure()\n", + " \n", + " # clients (size by weight)\n", + " cx = [client_xy[i][0] for i in clients]\n", + " cy = [client_xy[i][1] for i in clients]\n", + " cs = [25 + 10*w[i] for i in clients]\n", + " plt.scatter(cx, cy, s=cs, marker=\"o\", label=\"Clients\")\n", + " \n", + " # all candidate sites\n", + " sx = [site_xy[j][0] for j in sites]\n", + " sy = [site_xy[j][1] for j in sites]\n", + " plt.scatter(sx, sy, s=60, marker=\"^\", label=\"Candidate sites\")\n", + " \n", + " # opened sites\n", + " ox = [site_xy[j][0] for j in opened]\n", + " oy = [site_xy[j][1] for j in opened]\n", + " plt.scatter(ox, oy, s=200, marker=\"^\", label=\"Opened\", edgecolors=\"black\")\n", + " \n", + " # assignment lines\n", + " for i in clients:\n", + " j = assign[i]\n", + " plt.plot([client_xy[i][0], site_xy[j][0]],\n", + " [client_xy[i][1], site_xy[j][1]],\n", + " linewidth=0.5)\n", + " \n", + " plt.title(\"Pyomo p-Median: assignments to opened sites\")\n", + " plt.legend()\n", + " plt.show()\n", + "\n", + "opened, assign = print_summary(m)\n", + "plot_result(opened, assign)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Extension: Capacitated p-median\n", + "\n", + "If each station can handle at most K total demand weight:\n", + "\n", + "  ∑iwixij ≤ Kyj ∀j" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "K = 50\n", + "\n", + "# Add Constraint\n", + "def cap_rule(m, j):\n", + " return sum(m.w[i] * m.x[i, j] for i in m.I) <= K * m.y[j]\n", + "m.capacity = pyo.Constraint(m.J, rule=cap_rule)\n", + "\n", + "# Re-optimize\n", + "result = solver.solve(m)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "# Print summary and plot results\n", + "opened, assign = print_summary(m)\n", + "plot_result(opened, assign)" + ] + }, + { + "cell_type": "raw", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n", + "\n", + "SPDX-License-Identifier: Apache-2.0\n", + "\n", + "Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "you may not use this file except in compliance with the License.\n", + "You may obtain a copy of the License at\n", + "\n", + "http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software\n", + "distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "See the License for the specific language governing permissions and\n", + "limitations under the License.\n" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "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.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/portfolio_optimization/QP_portfolio_optimization.ipynb b/portfolio_optimization/QP_portfolio_optimization.ipynb new file mode 100644 index 0000000..93f009b --- /dev/null +++ b/portfolio_optimization/QP_portfolio_optimization.ipynb @@ -0,0 +1,700 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8055cf5b-4eb4-422a-b6fc-af885c4749c2", + "metadata": {}, + "source": [ + "# Portfolio Optimizer for Everyday Investors\n", + "\n", + "You have $10,000 in savings and want to invest across a few familiar assets. How do you balance higher expected return with the risk of large losses?\n", + "\n", + "This notebook builds a simple, realistic portfolio optimization workflow using **NVIDIA cuOpt's QP solver** for quadratic programming. We'll start with a small set of assets, compare equal-weight vs. optimized portfolios, and visualize the efficient frontier.\n", + "\n", + "**Key Concepts:**\n", + "- Mean-variance optimization (Markowitz portfolio theory)\n", + "- Efficient frontier visualization\n", + "- Interactive constraint exploration\n", + "\n", + "References:\n", + "- cuOpt LP/QP/MILP API Reference: https://docs.nvidia.com/cuopt/user-guide/latest/cuopt-python/lp-qp-milp/lp-qp-milp-api.html\n", + "- Portfolio Optimization: https://en.wikipedia.org/wiki/Portfolio_optimization" + ] + }, + { + "cell_type": "markdown", + "id": "fc98925c-ff8c-4767-bba0-16e193f97e23", + "metadata": {}, + "source": [ + "## Environment Setup and Installation\n", + "\n", + "### Install Required Dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96ea34e7-e559-47ce-93f4-daf2f6c34281", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import html\n", + "from IPython.display import display, HTML\n", + "\n", + "def check_gpu():\n", + " try:\n", + " result = subprocess.run([\"nvidia-smi\"], capture_output=True, text=True, timeout=5)\n", + " result.check_returncode()\n", + " lines = result.stdout.splitlines()\n", + " gpu_info = lines[2] if len(lines) > 2 else \"GPU detected\"\n", + " gpu_info_escaped = html.escape(gpu_info)\n", + " display(HTML(f\"\"\"\n", + "
\n", + "

✅ GPU is enabled

\n", + "
{gpu_info_escaped}
\n", + "
\n", + " \"\"\"))\n", + " return True\n", + " except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:\n", + " display(HTML(\"\"\"\n", + "
\n", + "

⚠️ GPU not detected!

\n", + "

This notebook requires a GPU runtime.

\n", + " \n", + "

If running in Google Colab:

\n", + "
    \n", + "
  1. Click on Runtime → Change runtime type
  2. \n", + "
  3. Set Hardware accelerator to GPU
  4. \n", + "
  5. Then click Save and Runtime → Restart runtime.
  6. \n", + "
\n", + " \n", + "

If running in Docker:

\n", + "
    \n", + "
  1. Ensure you have NVIDIA Docker runtime installed (nvidia-docker2)
  2. \n", + "
  3. Run container with GPU support: docker run --gpus all ...
  4. \n", + "
  5. Or use: docker run --runtime=nvidia ... for older Docker versions
  6. \n", + "
  7. Verify GPU access: docker run --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
  8. \n", + "
\n", + " \n", + "

Additional resources:

\n", + " \n", + "
\n", + " \"\"\"))\n", + " return False\n", + "\n", + "check_gpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18003f14-af20-44e8-b72a-381cc0312d3a", + "metadata": {}, + "outputs": [], + "source": [ + "# Enable this in case you are running this in google colab or such places where cuOpt is not yet installed\n", + "#!pip uninstall -y cuda-python cuda-bindings cuda-core\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu12 nvidia-nvjitlink-cu12 rapids-logger==0.1.19\n", + "#!pip install --upgrade --extra-index-url=https://pypi.nvidia.com cuopt-cu13 nvidia-nvjitlink-cu13 rapids-logger==0.1.19" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a56950d-d08c-43cd-a135-29aa17830223", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install --extra-index-url https://pypi.nvidia.com \"numpy>=1.24.4\" \"pandas>=2.2.1\"" + ] + }, + { + "cell_type": "markdown", + "id": "e0987b2f-dbb0-4411-9a54-d1567247398c", + "metadata": {}, + "source": [ + "### Import Required Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "449f51ca-33a0-463e-902f-2a1eecbedee7", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from cuopt.linear_programming.problem import (\n", + " Problem,\n", + " QuadraticExpression,\n", + " MINIMIZE,\n", + ")\n", + "\n", + "%config InlineBackend.figure_format = \"retina\"\n", + "\n", + "print(\"Imports ready (using cuOpt QP solver)\")" + ] + }, + { + "cell_type": "markdown", + "id": "b68072a1-8497-4d93-b275-ce7b3729673f", + "metadata": {}, + "source": [ + "## **Step 1:** Data Setup - A Small, Realistic Universe\n", + "\n", + "We'll work with a compact set of assets an everyday investor might recognize:\n", + "- Cash or money market\n", + "- US equity ETF proxy\n", + "- International equity ETF proxy\n", + "- Bond ETF proxy\n", + "- Real-asset/REIT or gold ETF proxy\n", + "\n", + "We'll simulate monthly returns with realistic annual return/volatility assumptions and correlations, then estimate the annualized mean returns and covariance matrix.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4beac288-fe86-40ee-9412-4b4004f233a3", + "metadata": {}, + "outputs": [], + "source": [ + "# Simulate monthly returns with realistic assumptions\n", + "np.random.seed(7)\n", + "\n", + "assets = [\"Cash\", \"US Equity\", \"Intl Equity\", \"Bond\", \"REIT/Gold\"]\n", + "\n", + "annual_mean = np.array([0.02, 0.08, 0.075, 0.04, 0.06])\n", + "annual_vol = np.array([0.005, 0.16, 0.18, 0.06, 0.14])\n", + "\n", + "corr = np.array([\n", + " [1.00, 0.05, 0.05, 0.10, 0.05],\n", + " [0.05, 1.00, 0.80, -0.10, 0.55],\n", + " [0.05, 0.80, 1.00, -0.05, 0.50],\n", + " [0.10, -0.10, -0.05, 1.00, 0.00],\n", + " [0.05, 0.55, 0.50, 0.00, 1.00],\n", + "])\n", + "\n", + "monthly_mean = annual_mean / 12.0\n", + "monthly_vol = annual_vol / np.sqrt(12.0)\n", + "monthly_cov = np.outer(monthly_vol, monthly_vol) * corr\n", + "\n", + "n_months = 120\n", + "returns = np.random.multivariate_normal(monthly_mean, monthly_cov, size=n_months)\n", + "\n", + "# Estimate annualized mean and covariance from the simulated data\n", + "mean_returns = returns.mean(axis=0) * 12.0\n", + "cov_matrix = np.cov(returns, rowvar=False) * 12.0\n", + "\n", + "summary = pd.DataFrame(\n", + " {\n", + " \"Annualized Return\": mean_returns,\n", + " \"Annualized Volatility\": np.sqrt(np.diag(cov_matrix)),\n", + " },\n", + " index=assets,\n", + ")\n", + "\n", + "summary.style.format({\"Annualized Return\": \"{:.2%}\", \"Annualized Volatility\": \"{:.2%}\"})" + ] + }, + { + "cell_type": "markdown", + "id": "68a3cddf-0a15-4dd2-9477-715b475b3c01", + "metadata": {}, + "source": [ + "## **Step 2:** Baseline Portfolio - Equal-Weight\n", + "\n", + "Before optimizing, let's compute the equal-weight portfolio. This gives a simple baseline to compare against the optimized portfolios." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6342042-e364-46e9-971b-344e3850766a", + "metadata": {}, + "outputs": [], + "source": [ + "def portfolio_stats(weights, mean_vec, cov_mat):\n", + " exp_return = float(weights @ mean_vec)\n", + " variance = float(weights @ cov_mat @ weights)\n", + " volatility = np.sqrt(variance)\n", + " return exp_return, volatility, variance\n", + "\n", + "n_assets = len(assets)\n", + "weights_equal = np.repeat(1.0 / n_assets, n_assets)\n", + "\n", + "ret_eq, vol_eq, var_eq = portfolio_stats(weights_equal, mean_returns, cov_matrix)\n", + "\n", + "baseline_df = pd.DataFrame(\n", + " {\n", + " \"Weight\": weights_equal,\n", + " \"Asset\": assets,\n", + " }\n", + ").set_index(\"Asset\")\n", + "\n", + "baseline_df, ret_eq, vol_eq\n" + ] + }, + { + "cell_type": "markdown", + "id": "660bc66a-6cbc-41b6-a0da-9c7eae657c11", + "metadata": {}, + "source": [ + "## **Step 3:** The Optimization Problem\n", + "\n", + "We translate the investor's goals into a quadratic program (QP):\n", + "\n", + "**Decision variables:** Portfolio weights $w_i$ for each asset $i$.\n", + "\n", + "**Objective:** Minimize portfolio variance (risk):\n", + "$$\\min \\frac{1}{2} w^\\top \\Sigma w$$\n", + "where $\\Sigma$ is the covariance matrix.\n", + "\n", + "**Constraints:**\n", + "- Fully invested: $\\sum_i w_i = 1$\n", + "- Long-only (no shorting): $w_i \\geq 0$\n", + "- Optional: target minimum return $\\mu^\\top w \\geq r_{target}$\n", + "- Optional: max allocation per asset $w_i \\leq w_{max}$\n", + "\n", + "### cuOpt QP Implementation\n", + "\n", + "cuOpt's QP solver allows us to express quadratic objectives using `Variable * Variable` syntax.\n", + "For the portfolio variance $w^\\top \\Sigma w$, we construct it as:\n", + "$$\\sum_i \\sum_j w_i \\cdot \\Sigma_{ij} \\cdot w_j$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04dd2427-69da-4a4e-80fd-2d1e5f796a74", + "metadata": {}, + "outputs": [], + "source": [ + "def solve_min_variance_qp(\n", + " cov_matrix,\n", + " mean_returns,\n", + " target_return=None,\n", + " max_weight=None,\n", + " min_safe_alloc=None,\n", + " safe_indices=None,\n", + "):\n", + " \"\"\"\n", + " Solve the minimum-variance portfolio problem using cuOpt QP solver.\n", + " \n", + " Parameters\n", + " ----------\n", + " cov_matrix : ndarray (n x n)\n", + " Annualized covariance matrix\n", + " mean_returns : ndarray (n,)\n", + " Annualized expected returns\n", + " target_return : float, optional\n", + " Minimum expected return constraint\n", + " max_weight : float, optional\n", + " Maximum weight for any single asset\n", + " min_safe_alloc : float, optional\n", + " Minimum combined allocation to safe assets\n", + " safe_indices : list of int, optional\n", + " Indices of safe assets (e.g., Cash, Bonds)\n", + " \n", + " Returns\n", + " -------\n", + " weights : ndarray\n", + " Optimal portfolio weights\n", + " portfolio_return : float\n", + " Expected return of the optimal portfolio\n", + " portfolio_vol : float\n", + " Volatility of the optimal portfolio\n", + " status : str\n", + " Solver status\n", + " \"\"\"\n", + " n = len(mean_returns)\n", + " \n", + " # Create cuOpt Problem\n", + " prob = Problem(\"Portfolio_Optimization\")\n", + " \n", + " # Decision variables: portfolio weights with bounds [0, upper_bound]\n", + " upper_bound = max_weight if max_weight is not None else 1.0\n", + " w = [prob.addVariable(lb=0.0, ub=upper_bound, name=f\"w_{i}\") for i in range(n)]\n", + " \n", + " # Build quadratic objective: minimize w' * cov_matrix * w\n", + " quad_expr = None\n", + " for i in range(n):\n", + " for j in range(n):\n", + " if abs(cov_matrix[i, j]) > 1e-12:\n", + " if(i==0 and j==0):\n", + " quad_expr = float(cov_matrix[i, j]) * w[i] * w[j]\n", + " else:\n", + " quad_expr += float(cov_matrix[i, j]) * w[i] * w[j]\n", + " \n", + " prob.setObjective(quad_expr, sense=MINIMIZE)\n", + " \n", + " # Constraint: Fully invested (sum of weights = 1)\n", + " sum_weights = sum(w)\n", + " prob.addConstraint(sum_weights == 1, name=\"fully_invested\")\n", + " \n", + " # Constraint: Minimum expected return\n", + " if target_return is not None:\n", + " expected_return_expr = sum(mean_returns[i] * w[i] for i in range(n))\n", + " prob.addConstraint(expected_return_expr >= target_return, name=\"min_return\")\n", + " \n", + " # Constraint: Minimum allocation to safe assets\n", + " if min_safe_alloc is not None and safe_indices is not None:\n", + " safe_sum = sum(w[i] for i in safe_indices)\n", + " prob.addConstraint(safe_sum >= min_safe_alloc, name=\"min_safe\")\n", + " \n", + " # Solve the problem\n", + " prob.solve()\n", + " \n", + " # Extract solution\n", + " weights = np.array([w[i].Value for i in range(n)])\n", + " \n", + " # Compute portfolio statistics\n", + " portfolio_return = float(mean_returns @ weights)\n", + " portfolio_vol = np.sqrt(float(weights @ cov_matrix @ weights))\n", + " \n", + " # Get solver status\n", + " status = \"optimal\" if prob.Status == 1 else f\"status_{prob.Status}\"\n", + " \n", + " return weights, portfolio_return, portfolio_vol, status\n", + "\n", + "print(\"Solver function defined (using cuOpt QP)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "519655b6-c792-4d52-842c-933e62632696", + "metadata": {}, + "source": [ + "## **Step 4:** Minimum-Variance Portfolio (No Return Target)\n", + "\n", + "First, let's find the portfolio with the lowest possible risk, without any constraint on expected return. This is the leftmost point on the efficient frontier." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16df1281-0a32-48b0-bcbb-4ffcf6fc48c7", + "metadata": {}, + "outputs": [], + "source": [ + "weights_mv, ret_mv, vol_mv, status_mv = solve_min_variance_qp(cov_matrix, mean_returns)\n", + "\n", + "print(f\"Status: {status_mv}\")\n", + "print(f\"\\nMinimum-Variance Portfolio:\")\n", + "print(f\" Expected Return: {ret_mv:.2%}\")\n", + "print(f\" Volatility: {vol_mv:.2%}\")\n", + "print(f\"\\nWeights:\")\n", + "for asset, wt in zip(assets, weights_mv):\n", + " print(f\" {asset:<12}: {wt:6.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "07ec7a0a-1521-4bea-94eb-38722be27218", + "metadata": {}, + "source": [ + "## **Step 5:** Efficient Frontier\n", + "\n", + "The efficient frontier shows all portfolios that offer the highest expected return for each level of risk. We trace it by solving QP problems for a range of target returns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8947c1d-a409-4b54-98f9-a171e845d961", + "metadata": {}, + "outputs": [], + "source": [ + "# Compute efficient frontier by sweeping target returns\n", + "min_ret = ret_mv\n", + "max_ret = mean_returns.max()\n", + "target_returns = np.linspace(min_ret, max_ret, 20)\n", + "\n", + "frontier_vols = []\n", + "frontier_rets = []\n", + "frontier_weights = []\n", + "\n", + "for target in target_returns:\n", + " w, r, v, _ = solve_min_variance_qp(cov_matrix, mean_returns, target_return=target)\n", + " frontier_vols.append(v)\n", + " frontier_rets.append(r)\n", + " frontier_weights.append(w)\n", + "\n", + "frontier_vols = np.array(frontier_vols)\n", + "frontier_rets = np.array(frontier_rets)\n", + "\n", + "print(f\"Computed {len(frontier_rets)} points on the efficient frontier.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed46fea8-2410-4f87-9bcb-00099eda9300", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=(9, 6))\n", + "\n", + "# Efficient frontier\n", + "ax.plot(frontier_vols * 100, frontier_rets * 100, \"b-\", lw=2, label=\"Efficient Frontier\")\n", + "\n", + "# Individual assets\n", + "asset_vols = np.sqrt(np.diag(cov_matrix))\n", + "ax.scatter(asset_vols * 100, mean_returns * 100, s=80, c=\"gray\", marker=\"o\", zorder=3, label=\"Individual Assets\")\n", + "for i, asset in enumerate(assets):\n", + " ax.annotate(asset, (asset_vols[i] * 100 + 0.3, mean_returns[i] * 100), fontsize=9)\n", + "\n", + "# Equal-weight portfolio\n", + "ax.scatter(vol_eq * 100, ret_eq * 100, s=120, c=\"orange\", marker=\"s\", zorder=4, label=\"Equal-Weight\")\n", + "\n", + "# Minimum-variance portfolio\n", + "ax.scatter(vol_mv * 100, ret_mv * 100, s=120, c=\"green\", marker=\"^\", zorder=4, label=\"Min-Variance\")\n", + "\n", + "ax.set_xlabel(\"Volatility (%)\")\n", + "ax.set_ylabel(\"Expected Return (%)\")\n", + "ax.set_title(\"Efficient Frontier: Risk vs. Return (cuOpt QP)\")\n", + "ax.legend(loc=\"lower right\")\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "2abad3d4-f354-4ffb-b516-74819d6a967f", + "metadata": {}, + "source": [ + "## **Step 6:** Constrained Portfolio - 5% Target Return + Diversification\n", + "\n", + "Now let's add practical constraints:\n", + "- Target at least 5% annual return\n", + "- No single asset > 50% (diversification)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba32fe1e-fed7-4220-a678-3ef709441f22", + "metadata": {}, + "outputs": [], + "source": [ + "target_5pct = 0.05\n", + "max_wt = 0.50\n", + "\n", + "weights_5pct, ret_5pct, vol_5pct, status_5pct = solve_min_variance_qp(\n", + " cov_matrix, mean_returns,\n", + " target_return=target_5pct,\n", + " max_weight=max_wt,\n", + ")\n", + "\n", + "print(f\"Status: {status_5pct}\")\n", + "print(f\"\\nConstrained Portfolio (Target >= 5%, Max Weight 50%):\")\n", + "print(f\" Expected Return: {ret_5pct:.2%}\")\n", + "print(f\" Volatility: {vol_5pct:.2%}\")\n", + "print(f\"\\nWeights:\")\n", + "for asset, wt in zip(assets, weights_5pct):\n", + " print(f\" {asset:<12}: {wt:6.1%}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "241d2789-6009-4e8f-aa5c-90dd516a09bb", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n", + "\n", + "# Bar chart of weights\n", + "x = np.arange(len(assets))\n", + "width = 0.35\n", + "\n", + "axes[0].bar(x - width/2, weights_equal * 100, width, label=\"Equal-Weight\", color=\"orange\")\n", + "axes[0].bar(x + width/2, weights_5pct * 100, width, label=\"Optimized (5%+ target)\", color=\"steelblue\")\n", + "axes[0].set_ylabel(\"Weight (%)\")\n", + "axes[0].set_xticks(x)\n", + "axes[0].set_xticklabels(assets, rotation=15)\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Portfolio Allocation Comparison\")\n", + "\n", + "# Scatter: return vs vol\n", + "axes[1].scatter(vol_eq * 100, ret_eq * 100, s=150, c=\"orange\", marker=\"s\", label=f\"Equal-Weight ({ret_eq:.1%}, {vol_eq:.1%})\")\n", + "axes[1].scatter(vol_5pct * 100, ret_5pct * 100, s=150, c=\"steelblue\", marker=\"^\", label=f\"Optimized ({ret_5pct:.1%}, {vol_5pct:.1%})\")\n", + "axes[1].plot(frontier_vols * 100, frontier_rets * 100, \"k--\", alpha=0.4, label=\"Frontier\")\n", + "axes[1].set_xlabel(\"Volatility (%)\")\n", + "axes[1].set_ylabel(\"Expected Return (%)\")\n", + "axes[1].legend()\n", + "axes[1].grid(True, alpha=0.3)\n", + "\n", + "axes[1].set_title(\"Return vs. Volatility\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "d0bd5fc4-037e-4c02-b8cc-927051b61de0", + "metadata": {}, + "source": [ + "## **Step 7:** Interactive Exploration - Adjust Your Preferences\n", + "\n", + "Now let's explore how changing your preferences affects the optimal portfolio. You can adjust:\n", + "- **Target return**: How much growth do you want?\n", + "- **Maximum weight per asset**: Avoid over-concentration\n", + "- **Minimum safe allocation**: Keep a floor in Cash + Bonds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02c36d23-ae18-46fe-a4b3-c9bc65452424", + "metadata": {}, + "outputs": [], + "source": [ + "# Interactive exploration function\n", + "# In Jupyter, uncomment the interact() call at the bottom to enable sliders\n", + "\n", + "def explore_portfolio(target_return_pct=5.0, max_weight_pct=60.0, min_safe_pct=10.0):\n", + " \"\"\"Explore portfolio with given parameters.\"\"\"\n", + " target_return = target_return_pct / 100.0\n", + " max_weight = max_weight_pct / 100.0\n", + " min_safe = min_safe_pct / 100.0\n", + " safe_indices = [0, 3] # Cash and Bond\n", + " \n", + " w, r, v, status = solve_min_variance_qp(\n", + " cov_matrix, mean_returns,\n", + " target_return=target_return,\n", + " max_weight=max_weight,\n", + " min_safe_alloc=min_safe,\n", + " safe_indices=safe_indices,\n", + " )\n", + " \n", + " fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + " \n", + " # Weights\n", + " colors = [\"#2ecc71\" if i in safe_indices else \"#3498db\" for i in range(len(assets))]\n", + " axes[0].barh(assets, w * 100, color=colors)\n", + " axes[0].set_xlabel(\"Weight (%)\")\n", + " axes[0].set_xlim(0, 70)\n", + " axes[0].set_title(\"Optimal Allocation\")\n", + " axes[0].axvline(max_weight * 100, color=\"red\", linestyle=\"--\", label=f\"Max {max_weight_pct:.0f}%\")\n", + " axes[0].legend()\n", + " \n", + " # Frontier with current point\n", + " axes[1].plot(frontier_vols * 100, frontier_rets * 100, \"b-\", lw=2, alpha=0.5)\n", + " axes[1].scatter(v * 100, r * 100, s=200, c=\"red\", marker=\"*\", zorder=5, label=\"Your Portfolio\")\n", + " axes[1].scatter(vol_eq * 100, ret_eq * 100, s=80, c=\"orange\", marker=\"s\", zorder=4, label=\"Equal-Weight\")\n", + " axes[1].set_xlabel(\"Volatility (%)\")\n", + " axes[1].set_ylabel(\"Expected Return (%)\")\n", + " axes[1].set_title(\"Your Portfolio on the Frontier\")\n", + " axes[1].legend()\n", + " axes[1].grid(True, alpha=0.3)\n", + " \n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + " print(f\"Status: {status}\")\n", + " print(f\"Expected Return: {r:.2%} | Volatility: {v:.2%}\")\n", + " print(f\"Safe Assets (Cash + Bond): {(w[0] + w[3]):.1%}\")\n", + "\n", + "# Demo with default parameters (static execution)\n", + "explore_portfolio(target_return_pct=5.0, max_weight_pct=60.0, min_safe_pct=10.0)\n", + "\n", + "# To enable interactive sliders in Jupyter, uncomment:\n", + "# from ipywidgets import interact, FloatSlider\n", + "# interact(\n", + "# explore_portfolio,\n", + "# target_return_pct=FloatSlider(value=5.0, min=2.0, max=8.0, step=0.5, description=\"Target Return %\"),\n", + "# max_weight_pct=FloatSlider(value=60.0, min=25.0, max=100.0, step=5.0, description=\"Max Weight %\"),\n", + "# min_safe_pct=FloatSlider(value=10.0, min=0.0, max=50.0, step=5.0, description=\"Min Safe %\"),\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "id": "e7b32527-5e24-4b71-afab-e075e80bfafb", + "metadata": {}, + "source": [ + "## **Step 8:** Interpretation - What It Means for You\n", + "\n", + "Let's put one scenario in plain English. Suppose you target a 6% annual return with at least 20% in safe assets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48f11500-0894-47a1-8dda-e5c06151379d", + "metadata": {}, + "outputs": [], + "source": [ + "target_scenario = 0.06\n", + "min_safe_scenario = 0.20\n", + "safe_indices = [0, 3]\n", + "\n", + "w_scenario, r_scenario, v_scenario, _ = solve_min_variance_qp(\n", + " cov_matrix, mean_returns,\n", + " target_return=target_scenario,\n", + " min_safe_alloc=min_safe_scenario,\n", + " safe_indices=safe_indices,\n", + ")\n", + "\n", + "print(\"=\" * 60)\n", + "print(\"YOUR RECOMMENDED PORTFOLIO\")\n", + "print(\"=\" * 60)\n", + "print(f\"\\nTarget: At least {target_scenario:.0%} annual return\")\n", + "print(f\"Constraint: Keep at least {min_safe_scenario:.0%} in Cash + Bonds\\n\")\n", + "\n", + "for asset, wt in zip(assets, w_scenario):\n", + " bar = \"█\" * int(wt * 40)\n", + " print(f\" {asset:<12} {wt:5.1%} {bar}\")\n", + "\n", + "print(f\"\\n→ Expected Return: {r_scenario:.2%}\")\n", + "print(f\"→ Volatility: {v_scenario:.2%}\")\n", + "print(f\"→ Safe Allocation: {(w_scenario[0] + w_scenario[3]):.1%}\")\n", + "\n", + "print(\"\\n\" + \"=\" * 60)\n", + "print(\"WHAT THIS MEANS\")\n", + "print(\"=\" * 60)\n", + "print(f\"\"\"\n", + "If you invest $10,000 according to this allocation:\n", + " - Cash: ${10000 * w_scenario[0]:,.0f}\n", + " - US Equity: ${10000 * w_scenario[1]:,.0f}\n", + " - Intl Equity: ${10000 * w_scenario[2]:,.0f}\n", + " - Bonds: ${10000 * w_scenario[3]:,.0f}\n", + " - REIT/Gold: ${10000 * w_scenario[4]:,.0f}\n", + "\n", + "You can expect ~{r_scenario:.1%} growth per year on average,\n", + "with a typical annual swing of about +/-{v_scenario:.1%}.\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.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/portfolio_optimization/README.md b/portfolio_optimization/README.md index 9bdf4a7..830e952 100644 --- a/portfolio_optimization/README.md +++ b/portfolio_optimization/README.md @@ -10,7 +10,11 @@ The portfolio optimization notebook solves a portfolio optimization problem wher - The goal is to maximize the expected return of a portfolio while minimizing the risk. -### 2. Advanced Portfolio Optimization +### 2. Portfolio Optimization using QP + +- The aim is to balance expected return with the risk of losses + +### 3. Advanced Portfolio Optimization For advanced portfolio optimization examples including: - Efficient frontier construction @@ -18,4 +22,4 @@ For advanced portfolio optimization examples including: - Turnover optimization - Mean-CVaR optimization with comprehensive workflows -Please visit the **[NVIDIA Quantitative Portfolio Optimization repository](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization)** \ No newline at end of file +Please visit the **[NVIDIA Quantitative Portfolio Optimization repository](https://github.com/NVIDIA-AI-Blueprints/quantitative-portfolio-optimization)** diff --git a/portfolio_optimization/cvar_portfolio_optimization.ipynb b/portfolio_optimization/cvar_portfolio_optimization.ipynb index ced1112..e07f534 100644 --- a/portfolio_optimization/cvar_portfolio_optimization.ipynb +++ b/portfolio_optimization/cvar_portfolio_optimization.ipynb @@ -663,7 +663,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cuopt", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -677,9 +677,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.13.11" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 }