Skip to main content
IBM Quantum Platform

Optimisation dynamique de portefeuille avec le Portfolio Optimizer de Global Data Quantum

Remarque

Les fonctions Qiskit sont une fonctionnalité expérimentale disponible uniquement pour IBM Quantum® Premium Plan, Flex Plan, et On-Prem (via IBM Quantum Platform API). Elles sont en cours de publication et peuvent être modifiées.

Estimation de l'utilisation : Environ 55 minutes sur un processeur Heron r2. (NOTE : Il s'agit uniquement d'une estimation. La durée d'exécution réelle peut varier.)


Arrière-plan

Le problème de l'optimisation dynamique de portefeuille vise à trouver la stratégie d'investissement optimale sur plusieurs périodes afin de maximiser le rendement attendu du portefeuille et de minimiser les risques, souvent sous certaines contraintes telles que le budget, les coûts de transaction ou l'aversion au risque. Contrairement à l'optimisation de portefeuille standard, qui considère un seul moment pour rééquilibrer le portefeuille, la version dynamique tient compte de la nature évolutive des actifs et adapte les investissements en fonction des changements dans la performance des actifs au fil du temps.

Ce tutoriel montre comment réaliser une optimisation dynamique de portefeuille en utilisant la fonction Qiskit de Quantum Portfolio Optimizer. Plus précisément, nous illustrons comment utiliser cette fonction d'application pour résoudre un problème d'allocation d'investissement sur plusieurs étapes temporelles.

L'approche consiste à formuler l'optimisation du portefeuille comme un problème multi-objectif d'optimisation binaire quadratique sans contrainte (QUBO). Plus précisément, nous formulons la fonction QUBO OO pour optimiser simultanément quatre objectifs différents :

  • Maximiser la fonction de retour FF
  • Minimiser le risque de l'investissement RR
  • Minimiser les coûts de transaction CC
  • Respecter les restrictions d'investissement, formulées dans une durée supplémentaire pour minimiser PP.

En résumé, pour atteindre ces objectifs, nous formulons la fonction QUBO comme suit O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P,γ\gamma est le coefficient d'aversion au risque et ρ\rho est le coefficient de renforcement des restrictions (multiplicateur de Lagrange). La formulation explicite se trouve dans l'Eq. (15) de notre manuscrit [1].

Nous résolvons le problème à l'aide d'une méthode hybride quantique-classique basée sur le Variational Quantum Eigensolver (VQE). Dans cette configuration, le circuit quantique estime la fonction de coût, tandis que l'optimisation classique est réalisée à l'aide de l'algorithme d'évolution différentielle, ce qui permet une navigation efficace dans le paysage des solutions. Le nombre de qubits requis dépend de trois facteurs principaux : le nombre d'actifs na, le nombre de périodes nt, et la résolution en bits utilisée pour représenter l'investissement nq. Plus précisément, le nombre minimum de qubits dans notre problème est na*nt*nq.

Pour ce tutoriel, nous nous concentrons sur l'optimisation d'un portefeuille régional basé sur l'indice espagnol IBEX 35. Plus précisément, nous utilisons un portefeuille de sept actifs, comme indiqué dans le tableau ci-dessous :

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

Nous rééquilibrons notre portefeuille en quatre étapes, chacune séparée par un intervalle de 30 jours à partir du 1er novembre 2022. Chaque variable d'investissement est codée sur deux bits. Il en résulte un problème dont la résolution nécessite 56 qubits.

Nous utilisons l'ansatz Optimized Real Amplitudes, une adaptation personnalisée et efficace du point de vue matériel de l'ansatz Real Amplitudes standard, spécialement conçu pour améliorer les performances pour ce type de problème d'optimisation financière.

L'exécution du quantum est réalisée sur le backend ibm_torino . Pour une explication détaillée de la formulation du problème, de la méthodologie et de l'évaluation des performances, veuillez vous référer au manuscrit publié [1].


Exigences

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

Configuration

Pour utiliser l'Optimiseur de portefeuille Quantum, sélectionnez la fonction dans le catalogue de fonctions Qiskit. Vous devez disposer d'un compte IBM Quantum Premium Plan ou Flex Plan avec une licence de Global Data Quantum pour exécuter cette fonction.

Commencez par vous authentifier à l'aide de votre clé API. Ensuite, chargez la fonction souhaitée à partir de l' Qiskit Functions Catalog Ici, vous accédez à la quantum_portfolio_optimizer fonction du catalogue à l'aide de la QiskitFunctionsCatalog classe. Cette fonction nous permet d'utiliser le solveur prédéfini « 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")

Étape 1 : Lire le portefeuille d'entrée

Dans cette étape, nous chargeons des données historiques pour les sept actifs sélectionnés de l'indice IBEX 35, en particulier du 1er novembre 2022 au 1er avril 2023.

Nous récupérons les données en utilisant l'API de Yahoo Finance, en nous concentrant sur les prix de clôture. Les données sont ensuite traitées pour s'assurer que tous les actifs ont le même nombre de jours avec des données. Les données manquantes (jours de non-négociation) sont traitées de manière appropriée, ce qui garantit que tous les actifs sont alignés sur les mêmes dates.

Les données sont structurées sur le site DataFrame avec un formatage cohérent pour tous les actifs.

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...
Avertissement

Nous vous recommandons d'exécuter la cellule suivante pour vérifier que les données ont été correctement téléchargées à partir de l'API Yahoo Finance. Si le téléchargement échoue, une notification indiquera que les données seront récupérées à partir d'une autre base de données en ligne.

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)

Étape 2 : Définir les données d'entrée du problème

Les paramètres nécessaires à la définition du problème QUBO sont configurés dans le dictionnaire qubo_settings . Nous définissons le nombre de pas de temps (nt), le nombre de bits pour la spécification de l'investissement (nq) et la fenêtre temporelle pour chaque pas de temps (dt). En outre, nous fixons l'investissement maximal par actif, le coefficient d'aversion au risque, les frais de transaction et le coefficient de restriction (voir notre article pour plus de détails sur la formulation du problème). Ces paramètres nous permettent d'adapter le problème QUBO au scénario d'investissement spécifique.

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,
}

Le optimizer_settings dictionnaire configure le processus d'optimisation, notamment en définissant des paramètres tels que num_generations pour le nombre d'itérations et population_size pour le nombre de solutions candidates par génération. D'autres paramètres permettent de contrôler des aspects tels que le taux de recombinaison, le nombre de tâches parallèles, la taille des lots et la plage de mutations. De plus, les paramètres de base, tels que estimator_shots, estimator_precision, et sampler_shots, définissent les configurations de l'estimateur et de l'échantillonneur quantiques pour le processus d'optimisation.

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

Le nombre total de circuits dépend des paramètres de optimizer_settings et est calculé comme suit : (num_generations + 1) * population_size.

Le ansatz_settings dictionnaire configure l'approximation du circuit quantique. Le ansatz paramètre spécifie l'utilisation de "optimized_real_amplitudes" l'approche, qui est une approche efficace sur le plan matériel conçue pour les problèmes d'optimisation financière. L'option multiple_passmanager , qui permet d'utiliser plusieurs gestionnaires de passes (y compris le gestionnaire de passes local Qiskit par défaut et le service de transcompilation alimenté par l'IA Qiskit) afin d'améliorer les performances et l'efficacité de l'optimisation des circuits, est actuellement définie sur false car cette fonctionnalité est temporairement désactivée.

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

Enfin, nous exécutons l'optimisation en lançant la fonction dpo_solver.run() , en passant par les entrées préparées. Il s'agit notamment du dictionnaire de données des actifs (assets), de la configuration de QUBO (qubo_settings), des paramètres d'optimisation (optimizer_settings) et des paramètres de l'ansatz du circuit quantique (ansatz_settings). En outre, nous spécifions les détails d'exécution tels que le backend et l'application éventuelle d'un post-traitement aux résultats. Le processus d'optimisation dynamique du portefeuille est alors lancé sur le backend quantique sélectionné.

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,
)

Étape 3 : Analyser les résultats de l'optimisation

Dans cette section, nous extrayons et affichons la solution ayant le coût objectif le plus bas à partir des résultats de l'optimisation. Outre le coût objectif minimum, nous présentons également les principales mesures associées à la solution correspondante, notamment l'écart de restriction, le ratio de Sharpe et le rendement de l'investissement.

# 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

Le code suivant montre comment visualiser et comparer la distribution des coûts d'un algorithme d'optimisation avec une distribution d'échantillonnage aléatoire. De même, nous explorons le paysage de la fonction objective QUBO (qui peut être chargée à partir de la sortie de la fonction) en l'évaluant avec des investissements aléatoires. Nous représentons graphiquement les deux distributions normalisées en amplitude afin de faciliter la comparaison entre le processus d'optimisation et l'échantillonnage aléatoire en termes de coût. De plus, le résultat obtenu à l'aide de DOCPlex est inclus sous forme de ligne de référence verticale en pointillés afin de servir de référence classique. Nous utilisons la version gratuite de DOCPlex — la IBM® bibliothèque open source pour l'optimisation mathématique dans Python — pour résoudre le même problème de manière classique.

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

Le graphique montre comment l'optimiseur quantique de portefeuille renvoie systématiquement des stratégies d'investissement optimisées.


Références

[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. « Mise à l'échelle du solveur variationnel des valeurs propres quantiques pour l' Portfolio Optimization dynamique » arXiv prépublication arXiv:2412.19150 (2024).


Enquête tutorielle

Veuillez prendre une minute pour nous faire part de vos commentaires sur ce tutoriel. Vos commentaires nous aideront à améliorer nos offres de contenu et l'expérience des utilisateurs. Lien vers l'enquête

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.