Skip to main content
IBM Quantum Platform

Ottimizzazione dinamica del portafoglio con Portfolio Optimizer di Global Data Quantum

Nota

Le funzioni Qiskit sono una funzione sperimentale disponibile solo per gli utenti di IBM Quantum® Premium Plan, Flex Plan e On-Prem (tramite IBM Quantum Platform API) Plan. Sono in stato di anteprima e sono soggetti a modifiche.

Stima di utilizzo: Circa 55 minuti con un processore Heron r2. (NOTA: questa è solo una stima. Il tempo di esecuzione effettivo potrebbe variare)


Sfondo

Il problema dell'ottimizzazione dinamica del portafoglio mira a trovare la strategia di investimento ottimale su più periodi di tempo per massimizzare il rendimento atteso del portafoglio e minimizzare i rischi, spesso sotto certi vincoli come il budget, i costi di transazione o l'avversione al rischio. A differenza dell'ottimizzazione di portafoglio standard, che considera un unico momento per riequilibrare il portafoglio, la versione dinamica tiene conto della natura evolutiva degli asset e adatta gli investimenti in base alle variazioni della performance degli asset nel tempo.

Questa esercitazione mostra come eseguire l'ottimizzazione dinamica del portafoglio utilizzando la funzione Qiskit del Quantum Portfolio Optimizer. In particolare, illustriamo come utilizzare questa funzione applicativa per risolvere un problema di allocazione degli investimenti su più fasi temporali.

L'approccio prevede la formulazione dell'ottimizzazione del portafoglio come un problema multi-obiettivo di ottimizzazione binaria quadratica non vincolata (QUBO). In particolare, formuliamo la funzione QUBO OO per ottimizzare simultaneamente quattro diversi obiettivi:

  • Massimizzare la funzione di ritorno FF
  • Ridurre al minimo il rischio dell'investimento RR
  • Ridurre al minimo i costi di transazione CC
  • Rispettare le restrizioni all'investimento, formulate in un termine aggiuntivo per minimizzare PP.

In sintesi, per affrontare questi obiettivi formuliamo la funzione QUBO come O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, dove γ\gamma è il coefficiente di avversione al rischio e ρ\rho è il coefficiente di rafforzamento delle restrizioni (moltiplicatore di Lagrange). La formulazione esplicita si trova in Eq. (15) del nostro manoscritto [1].

Risolviamo utilizzando un metodo ibrido quantistico-classico basato sul Variational Quantum Eigensolver (VQE). In questa configurazione, il circuito quantistico stima la funzione di costo, mentre l'ottimizzazione classica viene eseguita utilizzando l'algoritmo di evoluzione differenziale, consentendo una navigazione efficiente nel paesaggio delle soluzioni. Il numero di qubit necessari dipende da tre fattori principali: il numero di attività na, il numero di periodi di tempo nt e la risoluzione in bit utilizzata per rappresentare l'investimento nq. In particolare, il numero minimo di qubit nel nostro problema è na*nt*nq.

In questa esercitazione ci concentreremo sull'ottimizzazione di un portafoglio regionale basato sull'indice IBEX 35 spagnolo. In particolare, utilizziamo un portafoglio di sette attività, come indicato nella tabella seguente:

Portafoglio IBEX 35
ACS.MC
ITX.MC
FER.MC
ELE.MC
SCYR.MC
AENA.MC
AMS.MC

Riequilibriamo il nostro portafoglio in quattro fasi temporali, ciascuna separata da un intervallo di 30 giorni a partire dal 1° novembre 2022. Ogni variabile di investimento è codificata con due bit. Il risultato è un problema che richiede 56 qubit per essere risolto.

Utilizziamo l'Ansatz delle Ampiezze Reali Ottimizzate, un adattamento personalizzato ed efficiente dal punto di vista hardware dell'Ansatz delle Ampiezze Reali standard, studiato appositamente per migliorare le prestazioni di questo tipo di problemi di ottimizzazione finanziaria.

L'esecuzione quantistica viene eseguita sul backend ibm_torino . Per una spiegazione dettagliata della formulazione del problema, della metodologia e della valutazione delle prestazioni, si rimanda al manoscritto pubblicato [1].


Requisiti

!pip install qiskit-ibm-catalog
!pip install pandas
!pip install matplotlib
!pip install yfinance

Configura

Per utilizzare Quantum Portfolio Optimizer, selezionare la funzione tramite il catalogo delle funzioni di Qiskit. Per eseguire questa funzione è necessario un account IBM Quantum Premium Plan o Flex Plan con una licenza di Global Data Quantum.

Per prima cosa, effettua l'autenticazione con la tua chiave API. Quindi, carica la funzione desiderata dall' Qiskit Functions Catalog In questo caso, stai richiamando la quantum_portfolio_optimizer funzione dal catalogo utilizzando la QiskitFunctionsCatalog classe. Questa funzione ci permette di utilizzare il risolutore predefinito Quantum Portfolio Optimization.

from qiskit_ibm_catalog import QiskitFunctionsCatalog

catalog = QiskitFunctionsCatalog(
    channel="ibm_quantum_platform",
    instance="INSTANCE_CRN",
    # For `token`, use the 44-character API_KEY you created
    # and saved from the IBM Quantum Platform Home dashboard
    token="YOUR_API_KEY",
)

# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")

Passaggio 1: Leggi il portafoglio di input

In questa fase, carichiamo i dati storici dei sette asset selezionati dall'indice IBEX 35, in particolare dal 1° novembre 2022 al 1° aprile 2023.

I dati vengono recuperati utilizzando l'API di Yahoo Finance, concentrandosi sui prezzi di chiusura. I dati vengono quindi elaborati per garantire che tutti gli asset abbiano lo stesso numero di giorni con dati. I dati mancanti (giorni di non negoziazione) vengono gestiti in modo appropriato, garantendo che tutti gli asset siano allineati alle stesse date.

I dati sono strutturati in un sito DataFrame con una formattazione coerente per tutti gli asset.

import yfinance as yf
import pandas as pd

# List of IBEX 35 symbols
symbols = [
    "ACS.MC",
    "ITX.MC",
    "FER.MC",
    "ELE.MC",
    "SCYR.MC",
    "AENA.MC",
    "AMS.MC",
]

start_date = "2022-11-01"
end_date = "2023-4-01"

series_list = []
symbol_names = [symbol.replace(".", "_") for symbol in symbols]

# Create a full date index including weekends
full_index = pd.date_range(start=start_date, end=end_date, freq="D")

for symbol, name in zip(symbols, symbol_names):
    print(f"Downloading data for {symbol}...")
    data = yf.download(symbol, start=start_date, end=end_date)["Close"]
    data.name = name

    # Reindex to include weekends
    data = data.reindex(full_index)

    # Fill missing values (for example, weekends or holidays)
    # by forward/backward fill
    data.ffill(inplace=True)
    data.bfill(inplace=True)

    series_list.append(data)

# Combine all series into a single DataFrame
assets = pd.concat(series_list, axis=1)

# Convert index to string for consistency
assets.index = assets.index.astype(str)

Output:

[*********************100%***********************]  1 of 1 completed

[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
Downloading data for ACS.MC...
Downloading data for ITX.MC...
Downloading data for FER.MC...
Downloading data for ELE.MC...
Downloading data for SCYR.MC...
Downloading data for AENA.MC...
Downloading data for AMS.MC...
Avvertenza

Si consiglia di eseguire la seguente cella per verificare che i dati siano stati scaricati correttamente dall'API di Yahoo Finance. Se il download non riesce, una notifica indicherà che i dati saranno invece recuperati da un database online alternativo.

if (
    assets.isna().values.any()
):  # If has nan values, load the data from the fallback online database
    print(
        "WARNING: Asset data format is invalid or incomplete. "
        "Loading data from the remote backup database."
    )

    url = "https://raw.githubusercontent.com/Global-Data-Quantum/PortfolioData/1f3ebb95fdef245014a04d4273f688a2951e0061/data/qpo_tutorial_ibex.csv"
    assets = pd.read_csv(url, index_col=0)

Fase 2: Definire gli input del problema

I parametri necessari per definire il problema QUBO sono configurati nel dizionario qubo_settings . Definiamo il numero di passi temporali (nt), il numero di bit per la specificazione dell'investimento (nq) e la finestra temporale per ogni passo temporale (dt). Inoltre, fissiamo l'investimento massimo per attività, il coefficiente di avversione al rischio, la commissione di transazione e il coefficiente di restrizione (si veda il nostro articolo per i dettagli sulla formulazione del problema). Queste impostazioni ci permettono di adattare il problema QUBO allo specifico scenario di investimento.

qubo_settings = {
    "nt": 4,
    "nq": 2,
    "dt": 30,
    # maximum investment per asset is 2**nq/max_investment = 80%
    "max_investment": 5,
    "risk_aversion": 1000.0,
    "transaction_fee": 0.01,
    "restriction_coeff": 1.0,
}

Il optimizer_settings dizionario configura il processo di ottimizzazione, inclusi parametri quali num_generations per il numero di iterazioni e population_size per il numero di soluzioni candidate per generazione. Altre impostazioni regolano aspetti quali il tasso di ricombinazione, i processi in parallelo, la dimensione dei batch e l'intervallo di mutazione. Inoltre, le impostazioni primitive, quali estimator_shots, estimator_precision, e sampler_shots, definiscono le configurazioni dello stimatore e del campionatore quantistico per il processo di ottimizzazione.

optimizer_settings = {
    "de_optimizer_settings": {
        "num_generations": 20,
        "population_size": 40,
        "recombination": 0.4,
        "max_parallel_jobs": 5,
        "max_batchsize": 4,
        "mutation_range": [0.0, 0.25],
    },
    "optimizer": "differential_evolution",
    "primitive_settings": {
        "estimator_shots": 25_000,
        "estimator_precision": None,
        "sampler_shots": 100_000,
    },
}
Note

Il numero totale di circuiti dipende dai parametri di optimizer_settings ed è calcolato come (num_generations + 1) * population_size.

Il ansatz_settings dizionario configura l'ansatz del circuito quantistico. Il ansatz parametro specifica l'uso "optimized_real_amplitudes" dell'approccio, che è un approccio efficiente dal punto di vista hardware progettato per problemi di ottimizzazione finanziaria. L'opzione multiple_passmanager , che consente l'uso di più gestori di passaggi (incluso il gestore di passaggi locale predefinito Qiskit e il servizio di transpilazione basato sull'intelligenza artificiale Qiskit) per migliorare le prestazioni e l'efficienza dell'ottimizzazione dei circuiti, è attualmente impostata su false perché questa funzione è temporaneamente disabilitata.

ansatz_settings = {
    "ansatz": "optimized_real_amplitudes",
    "multiple_passmanager": False,
}

Infine, eseguiamo l'ottimizzazione eseguendo la funzione dpo_solver.run() , passando gli input preparati. Questi includono il dizionario dei dati delle risorse (assets), la configurazione di QUBO (qubo_settings), i parametri di ottimizzazione (optimizer_settings) e le impostazioni dell'ansatz dei circuiti quantistici (ansatz_settings). Inoltre, si specificano i dettagli dell'esecuzione, come il backend e l'eventuale applicazione della post-elaborazione ai risultati. Questo avvia il processo di ottimizzazione dinamica del portafoglio sul backend quantistico selezionato.

dpo_job = dpo_solver.run(
    assets=assets.to_dict(),  # Insert assets as a dictionary
    qubo_settings=qubo_settings,
    optimizer_settings=optimizer_settings,
    ansatz_settings=ansatz_settings,
    backend_name="ibm_torino",
    previous_session_id=[],
    apply_postprocess=True,
)

Fase 3: Analizzare i risultati dell'ottimizzazione

In questa sezione, estraiamo e visualizziamo la soluzione con il costo obiettivo più basso dai risultati dell'ottimizzazione. Oltre al costo obiettivo minimo, presentiamo anche le metriche chiave associate alla soluzione associata, tra cui la deviazione della restrizione, il rapporto Sharpe e il rendimento dell'investimento.

# Get the results of the job
dpo_result = dpo_job.result()

# Show the solution strategy
dpo_result["result"]

Output:

{'time_step_0': {'ACS.MC': 0.11764705882352941,
  'ITX.MC': 0.20588235294117646,
  'FER.MC': 0.38235294117647056,
  'ELE.MC': 0.058823529411764705,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.058823529411764705,
  'AMS.MC': 0.17647058823529413},
 'time_step_1': {'ACS.MC': 0.11428571428571428,
  'ITX.MC': 0.14285714285714285,
  'FER.MC': 0.2,
  'ELE.MC': 0.02857142857142857,
  'SCYR.MC': 0.42857142857142855,
  'AENA.MC': 0.0,
  'AMS.MC': 0.08571428571428572},
 'time_step_2': {'ACS.MC': 0.0,
  'ITX.MC': 0.09375,
  'FER.MC': 0.3125,
  'ELE.MC': 0.34375,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.0,
  'AMS.MC': 0.25},
 'time_step_3': {'ACS.MC': 0.3939393939393939,
  'ITX.MC': 0.09090909090909091,
  'FER.MC': 0.12121212121212122,
  'ELE.MC': 0.18181818181818182,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.0,
  'AMS.MC': 0.21212121212121213}}
import pandas as pd

# Get results from the job
dpo_result = dpo_job.result()

# Convert metadata to a DataFrame, excluding 'session_id'
df = pd.DataFrame(dpo_result["metadata"]["all_samples_metrics"])

# Find the minimum objective cost
min_cost = df["objective_costs"].min()
print(f"Minimum Objective Cost Found: {min_cost:.2f}")

# Extract the row with the lowest cost
best_row = df[df["objective_costs"] == min_cost].iloc[0]

# Display the results associated with the best solution
print("Best Solution:")
print(f"  - Restriction Deviation: {best_row['rest_breaches']}%")
print(f"  - Sharpe Ratio: {best_row['sharpe_ratios']:.2f}")
print(f"  - Return: {best_row['returns']:.2f}")

Output:

Minimum Objective Cost Found: -3.67
Best Solution:
  - Restriction Deviation: 40.0%
  - Sharpe Ratio: 14.54
  - Return: 0.28

Il codice seguente mostra come visualizzare e confrontare la distribuzione dei costi di un algoritmo di ottimizzazione con una distribuzione campionaria casuale. Allo stesso modo, esploriamo il panorama della funzione obiettivo QUBO (che può essere caricata dall'output della funzione) valutandola con investimenti casuali. Tracciamo entrambe le distribuzioni normalizzate in ampiezza per facilitare il confronto tra il processo di ottimizzazione e il campionamento casuale in termini di costo. Inoltre, il risultato ottenuto utilizzando DOCPlex è incluso come linea di riferimento verticale tratteggiata che funge da benchmark classico. Utilizziamo la versione gratuita di DOCPlex — la IBM® libreria open source per l'ottimizzazione matematica in Python — per risolvere lo stesso problema in modo classico.

import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
import matplotlib.patheffects as patheffects


def plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized):
    """
    Plots normalized results for two sampling results.

    Parameters:
        dpo_x (array-like): X-values for the VQE Post-processed curve.
        dpo_y_normalized (array-like): Y-values (normalized) for the
        VQE Post-processed curve.
        random_x (array-like): X-values for the Noise (Random) curve.
        random_y_normalized (array-like): Y-values (normalized) for the
        Noise (Random) curve.
    """
    plt.figure(figsize=(6, 3))
    plt.tick_params(axis="both", which="major", labelsize=12)

    # Define custom colors
    colors = ["#4823E8", "#9AA4AD"]

    # Plot DPO results
    (line1,) = plt.plot(
        dpo_x, dpo_y_normalized, label="VQE Postprocessed", color=colors[0]
    )
    line1.set_path_effects(
        [patheffects.withStroke(linewidth=3, foreground="white")]
    )

    # Plot Random results
    (line2,) = plt.plot(
        random_x, random_y_normalized, label="Noise (Random)", color=colors[1]
    )
    line2.set_path_effects(
        [patheffects.withStroke(linewidth=3, foreground="white")]
    )

    # Set X-axis ticks to increment by 5 units
    plt.gca().xaxis.set_major_locator(MultipleLocator(5))

    # Axis labels and legend
    plt.xlabel("Objective cost", fontsize=14)
    plt.ylabel("Normalized Counts", fontsize=14)

    # Add DOCPLEX reference line
    plt.axvline(
        x=-4.11, color="black", linestyle="--", linewidth=1, label="DOCPlex"
    )  # DOCPlex value
    plt.ylim(bottom=0)

    plt.legend()

    # Adjust layout
    plt.tight_layout()
    plt.show()
import numpy as np
from collections import defaultdict

# ================================
# STEP 1: DPO COST DISTRIBUTION
# ================================

# Extract data from DPO results
counts_list = dpo_result["metadata"]["all_samples_metrics"][
    "counts"
]  # List of how many times each solution occurred
cost_list = dpo_result["metadata"]["all_samples_metrics"][
    "objective_costs"
]  # List of corresponding objective function values (costs)

# Round costs to one decimal and accumulate counts for each unique cost
dpo_counter = defaultdict(int)
for cost, count in zip(cost_list, counts_list):
    rounded_cost = round(cost, 1)
    dpo_counter[rounded_cost] += count

# Prepare data for plotting
dpo_x = sorted(dpo_counter.keys())  # Sorted list of cost values
dpo_y = [dpo_counter[c] for c in dpo_x]  # Corresponding counts

# Normalize the counts to the range [0, 1] for better comparison
dpo_min = min(dpo_y)
dpo_max = max(dpo_y)
dpo_y_normalized = [
    (count - dpo_min) / (dpo_max - dpo_min) for count in dpo_y
]

# ================================
# STEP 2: RANDOM COST DISTRIBUTION
# ================================

# Read the QUBO matrix
qubo = np.array(dpo_result["metadata"]["qubo"])

bitstring_length = qubo.shape[0]
num_random_samples = 100_000  # Number of random samples to generate
random_cost_counter = defaultdict(int)

# Generate random bitstrings and calculate their cost
for _ in range(num_random_samples):
    x = np.random.randint(0, 2, size=bitstring_length)
    cost = float(x @ qubo @ x.T)
    rounded_cost = round(cost, 1)
    random_cost_counter[rounded_cost] += 1

# Prepare random data for plotting
random_x = sorted(random_cost_counter.keys())
random_y = [random_cost_counter[c] for c in random_x]

# Normalize the random cost distribution
random_min = min(random_y)
random_max = max(random_y)
random_y_normalized = [
    (count - random_min) / (random_max - random_min) for count in random_y
]

# ================================
# STEP 3: PLOTTING
# ================================

plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized)

Output:

Output of the previous code cell

Il grafico mostra come l'ottimizzatore di portafoglio quantistico restituisca costantemente strategie di investimento ottimizzate.


Riferimenti

[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. "Scalabilità dell'Eigensolver quantistico variazionale per l' Portfolio Optimization dinamica." arXiv preprint arXiv:2412.19150 (2024).


Sondaggio tutorial

Dedicate un minuto a fornire un feedback su questa esercitazione. Le vostre indicazioni ci aiuteranno a migliorare la nostra offerta di contenuti e l'esperienza degli utenti. Collegamento al sondaggio

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.