{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d31e7ada-7707-4f25-9058-c895d1b205fc",
      "metadata": {},
      "source": [
        "---\n",
        "title: Dynamic portfolio optimization with Global Data Quantum's Portfolio Optimizer\n",
        "description: Solve a utility scale dynamic optimization problem using the Global Data Quantum's Quantum Portfolio Optimizer.\n",
        "---\n",
        "\n",
        "{/* cspell:ignore fontsize rebalance patheffects labelsize */}\n",
        "\n",
        "# Dynamic portfolio optimization with Global Data Quantum's Portfolio Optimizer\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "36a8cc7a-179a-45f7-b28e-a2b4bbeaabd3",
      "metadata": {},
      "source": [
        "<Admonition type=\"note\" title=\"Note\">\n",
        "  Qiskit Functions are an experimental feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (via IBM Quantum Platform API) Plan users. They are in preview release status and subject to change.\n",
        "</Admonition>\n",
        "\n",
        "*Usage estimate: Approximately 55 minutes on a Heron r2 processor. (NOTE: This is an estimate only. Actual runtime might vary.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16f9751a-3f70-4c1c-a384-78f402567279",
      "metadata": {},
      "source": [
        "## Background\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "80d86936-8073-415e-a024-2adbd0efef2a",
      "metadata": {},
      "source": [
        "The dynamic portfolio optimization problem aims to find the optimal investment strategy over multiple time periods to maximize the expected portfolio return and minimize risks, often under certain constraints such as budget, transaction costs, or risk aversion. Unlike standard portfolio optimization, which considers a single time to rebalance the portfolio, the dynamic version accounts for the evolving nature of assets and adapts investments based on changes in asset performance over time.\n",
        "\n",
        "This tutorial demonstrates how to perform dynamic portfolio optimization by using the Quantum Portfolio Optimizer Qiskit Function. Specifically, we illustrate how to use this application function to solve an investment allocation problem over multiple time steps.\n",
        "\n",
        "The approach involves formulating the portfolio optimization as a multi-objective Quadratic Unconstrained Binary Optimization (QUBO) problem. Specifically, we formulate the QUBO function $O$ to simultaneously optimize four different objectives:\n",
        "\n",
        "* Maximize the return function $F$\n",
        "* Minimize the risk of the investment $R$\n",
        "* Minimize the transaction costs $C$\n",
        "* Comply with the investment restrictions, formulated in an additional term to minimize $P$.\n",
        "\n",
        "In summary, to tackle these objectives we formulate the QUBO function as\n",
        "$O = -F + \\frac{\\gamma}{2} R + C + \\rho P,$\n",
        "where $\\gamma$ is the risk aversion coefficient and $\\rho$ is the restrictions reinforcement coefficient (Lagrange multiplier). The explicit formulation can be found in Eq. (15) of our manuscript [\\[1\\]](#references).\n",
        "\n",
        "We solve using a hybrid quantum-classical method based on the Variational Quantum Eigensolver (VQE). In this setup, the quantum circuit estimates the cost function, while the classical optimization is performed using the Differential Evolution algorithm, enabling efficient navigation of the solution landscape. The number of qubits required depends on three main factors: the number of assets `na`, the number of time periods `nt`, and the bit resolution used to represent the investment `nq`. Specifically, the minimum number of qubits in our problem is `na*nt*nq`.\n",
        "\n",
        "For this tutorial, we focus on optimizing a regional portfolio based on the Spanish IBEX 35 index. Specifically, we use a seven-asset portfolio as indicated in the table below:\n",
        "\n",
        "| **IBEX 35 Portfolio** | ACS.MC | ITX.MC | FER.MC | ELE.MC | SCYR.MC | AENA.MC | AMS.MC |\n",
        "| --------------------- | ------ | ------ | ------ | ------ | ------- | ------- | ------ |\n",
        "\n",
        "We rebalance our portfolio in four time steps, each separated by a 30-day interval  starting November 1, 2022. Each investment variable is encoded using two bits. This results in a problem that requires 56 qubits to solve.\n",
        "\n",
        "We use the Optimized Real Amplitudes ansatz, a customized and hardware-efficient adaptation of the standard Real Amplitudes ansatz, specifically tailored to improve performance for this type of financial optimization problem.\n",
        "\n",
        "Quantum execution is performed on the `ibm_torino` backend. For a detailed explanation of the problem formulation, methodology, and performance evaluation, refer to the published manuscript [\\[1\\]](#references).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3716659d-a44f-448b-a131-e0044b05d2ed",
      "metadata": {},
      "source": [
        "## Requirements\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "49cddc2b-6759-4d18-aa1c-c38c344139d7",
      "metadata": {},
      "outputs": [],
      "source": [
        "!pip install qiskit-ibm-catalog\n",
        "!pip install pandas\n",
        "!pip install matplotlib\n",
        "!pip install yfinance"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ab6114f0-fad3-4f44-809d-3ff966f2b2ea",
      "metadata": {},
      "source": [
        "## Setup\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "689bf5f3-8332-4a7c-b62e-047566c1971f",
      "metadata": {},
      "source": [
        "To use the Quantum Portfolio Optimizer, select the function via the Qiskit Functions Catalog. You need an IBM Quantum Premium Plan or Flex Plan account with a license from Global Data Quantum to run this function.\n",
        "\n",
        "First, authenticate with your [API key](). Then, load the desired function from the Qiskit Functions Catalog. Here, you are accessing the `quantum_portfolio_optimizer` function from the catalog by using the `QiskitFunctionsCatalog` class. This function allows us to use the predefined Quantum Portfolio Optimization solver.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "301a8a04-3106-478b-816e-5d5f0a3c0737",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n",
        "\n",
        "catalog = QiskitFunctionsCatalog(\n",
        "    channel=\"ibm_quantum_platform\",\n",
        "    instance=\"INSTANCE_CRN\",\n",
        "    # For `token`, use the 44-character API_KEY you created\n",
        "    # and saved from the IBM Quantum Platform Home dashboard\n",
        "    token=\"YOUR_API_KEY\",\n",
        ")\n",
        "\n",
        "# Access function\n",
        "dpo_solver = catalog.load(\"global-data-quantum/quantum-portfolio-optimizer\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0856b7ca-8cf5-457d-ac08-df83a02852af",
      "metadata": {},
      "source": [
        "## Step 1: Read the input portfolio\n",
        "\n",
        "In this step, we load historical data for the seven selected assets from the IBEX 35 index, specifically from **November 1, 2022** to **April 1, 2023**.\n",
        "\n",
        "We fetch the data by using the Yahoo Finance API, focusing on closing prices. The data is then processed to ensure that all assets have the same number of days with data. Any missing data (non-trading days) is handled appropriately, ensuring all assets are aligned on the same dates.\n",
        "\n",
        "The data is structured in a DataFrame with consistent formatting across all assets.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "57ba1c16-4670-4557-9a16-2034be98f471",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "[*********************100%***********************]  1 of 1 completed"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "\n",
            "[*********************100%***********************]  1 of 1 completed\n",
            "[*********************100%***********************]  1 of 1 completed\n",
            "[*********************100%***********************]  1 of 1 completed\n",
            "[*********************100%***********************]  1 of 1 completed\n",
            "[*********************100%***********************]  1 of 1 completed\n",
            "[*********************100%***********************]  1 of 1 completed"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Downloading data for ACS.MC...\n",
            "Downloading data for ITX.MC...\n",
            "Downloading data for FER.MC...\n",
            "Downloading data for ELE.MC...\n",
            "Downloading data for SCYR.MC...\n",
            "Downloading data for AENA.MC...\n",
            "Downloading data for AMS.MC...\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "\n"
          ]
        }
      ],
      "source": [
        "import yfinance as yf\n",
        "import pandas as pd\n",
        "\n",
        "# List of IBEX 35 symbols\n",
        "symbols = [\n",
        "    \"ACS.MC\",\n",
        "    \"ITX.MC\",\n",
        "    \"FER.MC\",\n",
        "    \"ELE.MC\",\n",
        "    \"SCYR.MC\",\n",
        "    \"AENA.MC\",\n",
        "    \"AMS.MC\",\n",
        "]\n",
        "\n",
        "start_date = \"2022-11-01\"\n",
        "end_date = \"2023-4-01\"\n",
        "\n",
        "series_list = []\n",
        "symbol_names = [symbol.replace(\".\", \"_\") for symbol in symbols]\n",
        "\n",
        "# Create a full date index including weekends\n",
        "full_index = pd.date_range(start=start_date, end=end_date, freq=\"D\")\n",
        "\n",
        "for symbol, name in zip(symbols, symbol_names):\n",
        "    print(f\"Downloading data for {symbol}...\")\n",
        "    data = yf.download(symbol, start=start_date, end=end_date)[\"Close\"]\n",
        "    data.name = name\n",
        "\n",
        "    # Reindex to include weekends\n",
        "    data = data.reindex(full_index)\n",
        "\n",
        "    # Fill missing values (for example, weekends or holidays)\n",
        "    # by forward/backward fill\n",
        "    data.ffill(inplace=True)\n",
        "    data.bfill(inplace=True)\n",
        "\n",
        "    series_list.append(data)\n",
        "\n",
        "# Combine all series into a single DataFrame\n",
        "assets = pd.concat(series_list, axis=1)\n",
        "\n",
        "# Convert index to string for consistency\n",
        "assets.index = assets.index.astype(str)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "574e52d6-c87e-4eb4-a2b6-eee8954231aa",
      "metadata": {},
      "source": [
        "<Admonition type=\"caution\" title=\"Warning\">\n",
        "  We recommend running the following cell to verify that the data has been downloaded from the Yahoo Finance API correctly. If the download fails, a notification will indicate that the data will instead be retrieved from an alternative online database.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "be78a997-678f-4bd0-a0c5-1a04c8a94afb",
      "metadata": {},
      "outputs": [],
      "source": [
        "if (\n",
        "    assets.isna().values.any()\n",
        "):  # If has nan values, load the data from the fallback online database\n",
        "    print(\n",
        "        \"WARNING: Asset data format is invalid or incomplete. \"\n",
        "        \"Loading data from the remote backup database.\"\n",
        "    )\n",
        "\n",
        "    url = \"https://raw.githubusercontent.com/Global-Data-Quantum/PortfolioData/1f3ebb95fdef245014a04d4273f688a2951e0061/data/qpo_tutorial_ibex.csv\"\n",
        "    assets = pd.read_csv(url, index_col=0)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fbd9f5aa-dc2d-48bb-805f-ae0d55aedc54",
      "metadata": {},
      "source": [
        "## Step 2: Define the problem inputs\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8f066377-62fb-46fd-ae1a-1058d99c5f7f",
      "metadata": {},
      "source": [
        "The parameters needed to define the QUBO problem are configured in the `qubo_settings` dictionary. We define the number of time steps (`nt`), the number of bits for investment specification (`nq`), and the time window for each time step (`dt`). Additionally, we set the maximum investment per asset, the risk aversion coefficient, the transaction fee, and the restriction coefficient (see [our paper](https://arxiv.org/pdf/2412.19150) for details on the problem formulation). These settings allow us to adapt the QUBO problem to the specific investment scenario.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d5e8137c-e3ef-45a7-928f-3bd6d02402cd",
      "metadata": {},
      "outputs": [],
      "source": [
        "qubo_settings = {\n",
        "    \"nt\": 4,\n",
        "    \"nq\": 2,\n",
        "    \"dt\": 30,\n",
        "    # maximum investment per asset is 2**nq/max_investment = 80%\n",
        "    \"max_investment\": 5,\n",
        "    \"risk_aversion\": 1000.0,\n",
        "    \"transaction_fee\": 0.01,\n",
        "    \"restriction_coeff\": 1.0,\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "06608c0a-34ac-47b4-84c5-da98458e6dab",
      "metadata": {},
      "source": [
        "The `optimizer_settings` dictionary configures the optimization process, including parameters such as `num_generations` for the number of iterations and `population_size` for the number of candidate solutions per generation. Other settings control aspects like the recombination rate, parallel jobs, batch size, and mutation range. Additionally, the primitive settings, such as `estimator_shots`, `estimator_precision`, and `sampler_shots`, define the quantum Estimator and Sampler configurations for the optimization process.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d4946f9e-91bf-49a2-9970-2239c877b544",
      "metadata": {},
      "outputs": [],
      "source": [
        "optimizer_settings = {\n",
        "    \"de_optimizer_settings\": {\n",
        "        \"num_generations\": 20,\n",
        "        \"population_size\": 40,\n",
        "        \"recombination\": 0.4,\n",
        "        \"max_parallel_jobs\": 5,\n",
        "        \"max_batchsize\": 4,\n",
        "        \"mutation_range\": [0.0, 0.25],\n",
        "    },\n",
        "    \"optimizer\": \"differential_evolution\",\n",
        "    \"primitive_settings\": {\n",
        "        \"estimator_shots\": 25_000,\n",
        "        \"estimator_precision\": None,\n",
        "        \"sampler_shots\": 100_000,\n",
        "    },\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fd9b0940-54b2-4ad1-833e-36b61b90cb46",
      "metadata": {},
      "source": [
        "<Admonition type=\"Note\">\n",
        "  The total number of circuits depends on the `optimizer_settings` parameters and is calculated as `(num_generations + 1) * population_size`.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "866224f8-c27e-43c6-8906-dab87d7a4219",
      "metadata": {},
      "source": [
        "The `ansatz_settings` dictionary configures the quantum circuit ansatz. The `ansatz` parameter specifies the use of the `\"optimized_real_amplitudes\"` approach, which is a hardware-efficient ansatz designed for financial optimization problems. The `multiple_passmanager` option, which enables the use of multiple pass managers (including the default local Qiskit pass manager and the Qiskit AI-powered transpiler service) to improve circuit optimization performance and efficiency, is currently set to false because this feature is temporarily disabled.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0054fbb0-75f0-468a-acc9-0066eabb85dd",
      "metadata": {},
      "outputs": [],
      "source": [
        "ansatz_settings = {\n",
        "    \"ansatz\": \"optimized_real_amplitudes\",\n",
        "    \"multiple_passmanager\": False,\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "76087a8d-a1f5-4adb-a96d-7c17b7c67cdf",
      "metadata": {},
      "source": [
        "Finally, we execute the optimization by running the `dpo_solver.run()` function, passing in the prepared inputs. These include the asset data dictionary (`assets`), the QUBO configuration (`qubo_settings`), optimization parameters (`optimizer_settings`), and the quantum circuit ansatz settings (`ansatz_settings`). Additionally, we specify the execution details such as the backend, and whether to apply post-processing to the results. This initiates the dynamic portfolio optimization process on the selected quantum backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c24af785-105d-45db-a242-3f215c845cf6",
      "metadata": {},
      "outputs": [],
      "source": [
        "dpo_job = dpo_solver.run(\n",
        "    assets=assets.to_dict(),  # Insert assets as a dictionary\n",
        "    qubo_settings=qubo_settings,\n",
        "    optimizer_settings=optimizer_settings,\n",
        "    ansatz_settings=ansatz_settings,\n",
        "    backend_name=\"ibm_torino\",\n",
        "    previous_session_id=[],\n",
        "    apply_postprocess=True,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7bc6ac05-5c0b-418e-9978-8dd5bdcd97c4",
      "metadata": {},
      "source": [
        "## Step 3: Analyze the optimization results\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0c248fb7-0605-4456-a5ac-ced65b7593e7",
      "metadata": {},
      "source": [
        "In this section, we extract and display the solution with the lowest objective cost from the optimization results. Along with the minimum objective cost, we also present key metrics associated with the associated solution, including the restriction deviation, Sharpe ratio, and investment return.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "3b41e88f-9f5e-4663-a225-ec732544a36a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'time_step_0': {'ACS.MC': 0.11764705882352941,\n",
              "  'ITX.MC': 0.20588235294117646,\n",
              "  'FER.MC': 0.38235294117647056,\n",
              "  'ELE.MC': 0.058823529411764705,\n",
              "  'SCYR.MC': 0.0,\n",
              "  'AENA.MC': 0.058823529411764705,\n",
              "  'AMS.MC': 0.17647058823529413},\n",
              " 'time_step_1': {'ACS.MC': 0.11428571428571428,\n",
              "  'ITX.MC': 0.14285714285714285,\n",
              "  'FER.MC': 0.2,\n",
              "  'ELE.MC': 0.02857142857142857,\n",
              "  'SCYR.MC': 0.42857142857142855,\n",
              "  'AENA.MC': 0.0,\n",
              "  'AMS.MC': 0.08571428571428572},\n",
              " 'time_step_2': {'ACS.MC': 0.0,\n",
              "  'ITX.MC': 0.09375,\n",
              "  'FER.MC': 0.3125,\n",
              "  'ELE.MC': 0.34375,\n",
              "  'SCYR.MC': 0.0,\n",
              "  'AENA.MC': 0.0,\n",
              "  'AMS.MC': 0.25},\n",
              " 'time_step_3': {'ACS.MC': 0.3939393939393939,\n",
              "  'ITX.MC': 0.09090909090909091,\n",
              "  'FER.MC': 0.12121212121212122,\n",
              "  'ELE.MC': 0.18181818181818182,\n",
              "  'SCYR.MC': 0.0,\n",
              "  'AENA.MC': 0.0,\n",
              "  'AMS.MC': 0.21212121212121213}}"
            ]
          },
          "execution_count": 8,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Get the results of the job\n",
        "dpo_result = dpo_job.result()\n",
        "\n",
        "# Show the solution strategy\n",
        "dpo_result[\"result\"]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "48b545ed-5350-4a72-b792-ad4c4400c833",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Minimum Objective Cost Found: -3.67\n",
            "Best Solution:\n",
            "  - Restriction Deviation: 40.0%\n",
            "  - Sharpe Ratio: 14.54\n",
            "  - Return: 0.28\n"
          ]
        }
      ],
      "source": [
        "import pandas as pd\n",
        "\n",
        "# Get results from the job\n",
        "dpo_result = dpo_job.result()\n",
        "\n",
        "# Convert metadata to a DataFrame, excluding 'session_id'\n",
        "df = pd.DataFrame(dpo_result[\"metadata\"][\"all_samples_metrics\"])\n",
        "\n",
        "# Find the minimum objective cost\n",
        "min_cost = df[\"objective_costs\"].min()\n",
        "print(f\"Minimum Objective Cost Found: {min_cost:.2f}\")\n",
        "\n",
        "# Extract the row with the lowest cost\n",
        "best_row = df[df[\"objective_costs\"] == min_cost].iloc[0]\n",
        "\n",
        "# Display the results associated with the best solution\n",
        "print(\"Best Solution:\")\n",
        "print(f\"  - Restriction Deviation: {best_row['rest_breaches']}%\")\n",
        "print(f\"  - Sharpe Ratio: {best_row['sharpe_ratios']:.2f}\")\n",
        "print(f\"  - Return: {best_row['returns']:.2f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "09116415-afbc-4e89-b3fd-9464b8f1d5df",
      "metadata": {},
      "source": [
        "The following code shows how to visualize and compare the cost distribution of an optimization algorithm with a random sampling distribution. Similarly, we explore the landscape of the QUBO objective function (which can be loaded from the function output) by evaluating it with random investments. We plot both distributions normalized in amplitude for easier comparison of how the optimization process differs from random sampling in terms of cost. Additionally, the result obtained using DOCPlex is included as a dashed vertical reference line to serve as a classical benchmark. We use the [free version of DOCPlex](https://ibmdecisionoptimization.github.io/docplex-doc/) — the IBM® open-source library for mathematical optimization in Python — to solve the same problem classically.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2dd1d015-3015-409f-b5d9-016c8ff592ce",
      "metadata": {},
      "outputs": [],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "from matplotlib.ticker import MultipleLocator\n",
        "import matplotlib.patheffects as patheffects\n",
        "\n",
        "\n",
        "def plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized):\n",
        "    \"\"\"\n",
        "    Plots normalized results for two sampling results.\n",
        "\n",
        "    Parameters:\n",
        "        dpo_x (array-like): X-values for the VQE Post-processed curve.\n",
        "        dpo_y_normalized (array-like): Y-values (normalized) for the\n",
        "        VQE Post-processed curve.\n",
        "        random_x (array-like): X-values for the Noise (Random) curve.\n",
        "        random_y_normalized (array-like): Y-values (normalized) for the\n",
        "        Noise (Random) curve.\n",
        "    \"\"\"\n",
        "    plt.figure(figsize=(6, 3))\n",
        "    plt.tick_params(axis=\"both\", which=\"major\", labelsize=12)\n",
        "\n",
        "    # Define custom colors\n",
        "    colors = [\"#4823E8\", \"#9AA4AD\"]\n",
        "\n",
        "    # Plot DPO results\n",
        "    (line1,) = plt.plot(\n",
        "        dpo_x, dpo_y_normalized, label=\"VQE Postprocessed\", color=colors[0]\n",
        "    )\n",
        "    line1.set_path_effects(\n",
        "        [patheffects.withStroke(linewidth=3, foreground=\"white\")]\n",
        "    )\n",
        "\n",
        "    # Plot Random results\n",
        "    (line2,) = plt.plot(\n",
        "        random_x, random_y_normalized, label=\"Noise (Random)\", color=colors[1]\n",
        "    )\n",
        "    line2.set_path_effects(\n",
        "        [patheffects.withStroke(linewidth=3, foreground=\"white\")]\n",
        "    )\n",
        "\n",
        "    # Set X-axis ticks to increment by 5 units\n",
        "    plt.gca().xaxis.set_major_locator(MultipleLocator(5))\n",
        "\n",
        "    # Axis labels and legend\n",
        "    plt.xlabel(\"Objective cost\", fontsize=14)\n",
        "    plt.ylabel(\"Normalized Counts\", fontsize=14)\n",
        "\n",
        "    # Add DOCPLEX reference line\n",
        "    plt.axvline(\n",
        "        x=-4.11, color=\"black\", linestyle=\"--\", linewidth=1, label=\"DOCPlex\"\n",
        "    )  # DOCPlex value\n",
        "    plt.ylim(bottom=0)\n",
        "\n",
        "    plt.legend()\n",
        "\n",
        "    # Adjust layout\n",
        "    plt.tight_layout()\n",
        "    plt.show()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "6b662682-279b-48b5-bc61-681846cf3c00",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/global-data-quantum-optimizer/extracted-outputs/6b662682-279b-48b5-bc61-681846cf3c00-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import numpy as np\n",
        "from collections import defaultdict\n",
        "\n",
        "# ================================\n",
        "# STEP 1: DPO COST DISTRIBUTION\n",
        "# ================================\n",
        "\n",
        "# Extract data from DPO results\n",
        "counts_list = dpo_result[\"metadata\"][\"all_samples_metrics\"][\n",
        "    \"counts\"\n",
        "]  # List of how many times each solution occurred\n",
        "cost_list = dpo_result[\"metadata\"][\"all_samples_metrics\"][\n",
        "    \"objective_costs\"\n",
        "]  # List of corresponding objective function values (costs)\n",
        "\n",
        "# Round costs to one decimal and accumulate counts for each unique cost\n",
        "dpo_counter = defaultdict(int)\n",
        "for cost, count in zip(cost_list, counts_list):\n",
        "    rounded_cost = round(cost, 1)\n",
        "    dpo_counter[rounded_cost] += count\n",
        "\n",
        "# Prepare data for plotting\n",
        "dpo_x = sorted(dpo_counter.keys())  # Sorted list of cost values\n",
        "dpo_y = [dpo_counter[c] for c in dpo_x]  # Corresponding counts\n",
        "\n",
        "# Normalize the counts to the range [0, 1] for better comparison\n",
        "dpo_min = min(dpo_y)\n",
        "dpo_max = max(dpo_y)\n",
        "dpo_y_normalized = [\n",
        "    (count - dpo_min) / (dpo_max - dpo_min) for count in dpo_y\n",
        "]\n",
        "\n",
        "# ================================\n",
        "# STEP 2: RANDOM COST DISTRIBUTION\n",
        "# ================================\n",
        "\n",
        "# Read the QUBO matrix\n",
        "qubo = np.array(dpo_result[\"metadata\"][\"qubo\"])\n",
        "\n",
        "bitstring_length = qubo.shape[0]\n",
        "num_random_samples = 100_000  # Number of random samples to generate\n",
        "random_cost_counter = defaultdict(int)\n",
        "\n",
        "# Generate random bitstrings and calculate their cost\n",
        "for _ in range(num_random_samples):\n",
        "    x = np.random.randint(0, 2, size=bitstring_length)\n",
        "    cost = float(x @ qubo @ x.T)\n",
        "    rounded_cost = round(cost, 1)\n",
        "    random_cost_counter[rounded_cost] += 1\n",
        "\n",
        "# Prepare random data for plotting\n",
        "random_x = sorted(random_cost_counter.keys())\n",
        "random_y = [random_cost_counter[c] for c in random_x]\n",
        "\n",
        "# Normalize the random cost distribution\n",
        "random_min = min(random_y)\n",
        "random_max = max(random_y)\n",
        "random_y_normalized = [\n",
        "    (count - random_min) / (random_max - random_min) for count in random_y\n",
        "]\n",
        "\n",
        "# ================================\n",
        "# STEP 3: PLOTTING\n",
        "# ================================\n",
        "\n",
        "plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "457ecd2e-50e5-49ea-b435-fb26e43a3266",
      "metadata": {},
      "source": [
        "The graph shows how the quantum portfolio optimizer consistently returns optimized investment strategies.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f996a6d-b696-4d33-b832-97bda30b3969",
      "metadata": {},
      "source": [
        "## References\n",
        "\n",
        "\\[1] [Nodar, Álvaro, Irene De León, Danel Arias, Ernesto Mamedaliev, María Esperanza Molina, Manuel Martín-Cordero, Senaida Hernández-Santana et al. \"Scaling the Variational Quantum Eigensolver for Dynamic Portfolio Optimization.\" arXiv preprint arXiv:2412.19150 (2024)](https://arxiv.org/pdf/2412.19150).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "819996e9-e98d-4753-9d6a-be0ab443088f",
      "metadata": {},
      "source": [
        "## Tutorial survey\n",
        "\n",
        "Please take a minute to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n",
        "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_3BLFkNVEuh0QBWm)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "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"
    },
    "hours": 1.5,
    "qpuSeconds": 3300
  },
  "nbformat": 4,
  "nbformat_minor": 2
}