Quantum Portfolio Optimizer : une Qiskit Function par Global Data Quantum
Voir la référence API
Les Qiskit Functions sont une fonctionnalité expérimentale réservée aux utilisateurs des plans IBM Quantum® Premium, Flex et On-Prem (via l'API IBM Quantum Platform). Elles sont en version préliminaire et peuvent évoluer.
Vue d'ensemble
Le Quantum Portfolio Optimizer est une Qiskit Function qui s'attaque au problème d'optimisation dynamique de portefeuille, un problème classique en finance visant à rééquilibrer périodiquement des investissements sur un ensemble d'actifs afin de maximiser les rendements et de minimiser les risques. En déployant des techniques d'optimisation quantique de pointe, cette fonction simplifie le processus pour que les utilisateurs, sans expertise particulière en informatique quantique, puissent profiter de ses avantages dans la recherche de trajectoires d'investissement optimales. Idéal pour les gestionnaires de portefeuille, les chercheurs en finance quantitative et les investisseurs individuels, cet outil permet le backtesting de stratégies de trading dans le cadre de l'optimisation de portefeuille.
Description de la fonction
Le Quantum Portfolio Optimizer utilise l'algorithme Variational Quantum Eigensolver (VQE) pour résoudre un problème d'optimisation binaire quadratique sans contrainte (QUBO), en traitant des problèmes d'optimisation dynamique de portefeuille. Il suffit de fournir les données de prix des actifs et de définir la contrainte d'investissement ; la fonction exécute ensuite le processus d'optimisation quantique et retourne un ensemble de trajectoires d'investissement optimisées.
Le processus comporte quatre étapes principales. D'abord, les données d'entrée sont transformées en un problème compatible quantique : on construit le QUBO du problème d'optimisation dynamique de portefeuille et on le convertit en un opérateur quantique (Hamiltonien d'Ising). Ensuite, le problème d'entrée et l'algorithme VQE sont adaptés pour s'exécuter sur le matériel quantique. Le VQE est alors exécuté sur ce matériel, et enfin, les résultats sont post-traités pour fournir les trajectoires d'investissement optimales. Le système intègre également un post-traitement tenant compte du bruit (basé sur SQD) afin de maximiser la qualité des résultats.
Cette Qiskit Function est basée sur le manuscrit publié par Global Data Quantum.
Prise en main
Authentifie-toi avec ta clé API et sélectionne la Qiskit Function comme suit. (Cet extrait suppose que tu as déjà enregistré ton compte dans ton environnement local.)
# Added by doQumentation — required packages for this notebook
!pip install -q pandas qiskit-ibm-catalog
from qiskit_ibm_catalog import QiskitFunctionsCatalog
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")
Exemple : optimisation dynamique de portefeuille avec sept actifs
Cet exemple montre comment exécuter la fonction d'optimisation dynamique de portefeuille (DPO) et ajuster ses paramètres pour des performances optimales. Il inclut des étapes détaillées pour affiner les paramètres et atteindre les résultats souhaités.
Ce cas implique sept actifs, quatre pas de temps et quatre qubits de résolution, pour un besoin total de 112 qubits.
1. Lire les actifs inclus dans le portefeuille
Si tous les actifs du portefeuille sont stockés dans un dossier à un chemin spécifique, tu peux les charger dans un pandas.DataFrame et le convertir en objet dict à l'aide de la fonction suivante.
import os
import glob
import pandas as pd
def read_and_join_csv(file_pattern):
"""
Reads multiple CSV files matching the file pattern and combines them into a single DataFrame.
Parameters:
file_pattern (str): The pattern to match CSV files.
Returns:
pd.DataFrame: Combined DataFrame with data from all CSV files.
"""
# Find all files matching the pattern
csv_files = glob.glob(file_pattern)
# Get the base file names without the .csv extension
file_names = [os.path.basename(f).replace(".csv", "") for f in csv_files]
# Read each CSV file into a DataFrame and set the first column as the index
df_list = [pd.read_csv(f).set_index("Unnamed: 0") for f in csv_files]
# Rename columns in each DataFrame to the base file names
for df, name in zip(df_list, file_names):
df.columns = [name]
# Combine all DataFrames into one by merging them side by side
combined_df = pd.concat(df_list, axis=1)
return combined_df
file_pattern = "route/to/folder/with/assets/data/*.csv"
assets = read_and_join_csv(file_pattern).to_dict()
Pour cet exemple, nous avons utilisé les actifs 8801.T, CLF, GBPJPY, ITX.MC, META, TMBMKDE-10Y et XS2239553048. La figure suivante illustre les données utilisées dans cet exemple, montrant l'évolution du prix de clôture journalier des actifs du 1er janvier au 1er septembre 2023.
Dans cet exemple, pour garantir l'uniformité entre les dates, nous avons comblé les jours non ouvrés avec le prix de clôture de la dernière date disponible. Cette étape est nécessaire car les actifs sélectionnés proviennent de différents marchés avec des jours de bourse variables, et il est essentiel de standardiser le jeu de données pour assurer la cohérence.
2. Définir le problème
Définis les spécifications du problème en configurant les paramètres du dictionnaire qubo_settings.
qubo_settings = {
"nt": 4,
"nq": 4,
"dt": 30,
"max_investment": 25,
"risk_aversion": 1000.0,
"transaction_fee": 0.01,
"restriction_coeff": 1.0,
}
3. Définir les paramètres de l'optimiseur et de l'ansatz (Optionnel)
Définis optionnellement des exigences spécifiques pour le processus d'optimisation, notamment le choix de l'optimiseur et de ses paramètres, ainsi que la spécification de la primitive et de ses configurations.
Pour l'ansatz Tailored, la taille de population choisie est basée sur des expériences précédentes montrant que cette valeur produit une optimisation stable et efficace.
Dans le cas de l'ansatz Real Amplitudes, tu peux suivre une relation linéaire entre la population_size et le nombre de qubits dans le circuit. À titre de règle empirique approximative, il est recommandé d'utiliser une population_size minimale ~ 0.8 * n_qubits pour l'ansatz real_amplitudes.
On s'attend à ce que l'Optimized Real Amplitudes offre de meilleures performances d'optimisation que l'ansatz Real Amplitudes. Cependant, le nombre de variables à optimiser dans cet ansatz augmente bien plus vite que dans le cas Real Amplitudes (voir le manuscrit). Ainsi, pour les grands problèmes, l'Optimized Real Amplitudes nécessite davantage d'exécutions de circuits. Il est susceptible d'être utile pour des problèmes jusqu'à 100 qubits, mais il est recommandé d'être attentif lors du réglage des paramètres population_size. À titre d'exemple de cette mise à l'échelle de population_size, le tableau précédent montre que pour un problème à 84 qubits, l'Optimized Real Amplitudes requiert une population_size de 120, tandis que pour un problème à 56 qubits, une population_size de 40 suffit.
optimizer_settings = {
"de_optimizer_settings": {
"num_generations": 20,
"population_size": 90,
"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,
},
}
Il est également possible de choisir un ansatz spécifique. L'exemple suivant utilise l'ansatz 'Tailored'.
ansatz_settings = {
"ansatz": "tailored",
"multiple_passmanager": False,
}
4. Exécuter le problème
dpo_job = dpo_solver.run(
assets=assets,
qubo_settings=qubo_settings,
optimizer_settings=optimizer_settings,
ansatz_settings=ansatz_settings,
backend_name="<backend name>",
previous_session_id=[],
apply_postprocess=True,
)
5. Récupérer les résultats
La fonction retourne un dictionnaire avec les trajectoires d'investissement triées du plus bas au plus élevé selon leur valeur de fonction objectif (voir la section Sortie de la référence API). Cet ensemble de résultats permet d'identifier la trajectoire au coût le plus bas et ses évaluations d'investissement correspondantes. De plus, il permet d'analyser différentes trajectoires, facilitant la sélection de celles qui correspondent le mieux à des besoins ou objectifs spécifiques. Cette flexibilité garantit que les choix peuvent être adaptés à une grande variété de préférences ou de scénarios. Commence par présenter la stratégie résultante ayant atteint le coût objectif le plus bas trouvé au cours du processus.
# Get the results of the job
dpo_result = dpo_job.result()
# Show the solution strategy
dpo_result["result"]
{'time_step_0': {'8801.T': 0.11764705882352941,
'ITX.MC': 0.20588235294117646,
'META': 0.38235294117647056,
'GBPJPY=X': 0.058823529411764705,
'TMBMKDE-10Y': 0.0,
'CLF': 0.058823529411764705,
'XS2239553048': 0.17647058823529413},
'time_step_1': {'8801.T': 0.11428571428571428,
'ITX.MC': 0.14285714285714285,
'META': 0.2,
'GBPJPY=X': 0.02857142857142857,
'TMBMKDE-10Y': 0.42857142857142855,
'CLF': 0.0,
'XS2239553048': 0.08571428571428572},
'time_step_2': {'8801.T': 0.0,
'ITX.MC': 0.09375,
'META': 0.3125,
'GBPJPY=X': 0.34375,
'TMBMKDE-10Y': 0.0,
'CLF': 0.0,
'XS2239553048': 0.25},
'time_step_3': {'8801.T': 0.3939393939393939,
'ITX.MC': 0.09090909090909091,
'META': 0.12121212121212122,
'GBPJPY=X': 0.18181818181818182,
'TMBMKDE-10Y': 0.0,
'CLF': 0.0,
'XS2239553048': 0.21212121212121213}}
Ensuite, grâce aux métadonnées, tu peux accéder aux résultats de toutes les stratégies échantillonnées. Tu peux ainsi analyser plus en détail les trajectoires alternatives retournées par l'optimiseur. Pour ce faire, lis le dictionnaire stocké dans dpo_result['metadata']['all_samples_metrics'], qui contient non seulement des informations supplémentaires sur la stratégie optimale, mais aussi des détails sur les autres stratégies candidates évaluées durant l'optimisation.
L'exemple suivant montre comment lire ces informations avec pandas pour extraire les métriques clés associées à la stratégie optimale. Celles-ci comprennent la déviation de contrainte, le ratio de Sharpe et le rendement d'investissement correspondant.
# Convert metadata to a DataFrame
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']}")
Minimum Objective Cost Found: -3.78
Best Solution:
- Restriction Deviation: 40.0
- Sharpe Ratio: 24.82
- Return: 0.46
6. Analyse des performances
Enfin, analyse les performances de ton application d'optimisation. Compare notamment tes résultats, obtenus dans l'exemple précédent, à une base de référence aléatoire pour évaluer l'efficacité de l'approche. Si l'algorithme quantique produit de manière démontrable et cohérente des résultats avec des valeurs de coût plus basses, cela indique un processus d'optimisation efficace.
La figure présente les distributions de probabilité des coûts objectifs. Pour générer ces distributions, on prend la liste des coûts objectifs issus du résultat de la fonction et on compte les occurrences de chaque valeur de coût (valeurs arrondies au deuxième décimal). On met ensuite à jour la colonne de comptage en regroupant les occurrences de valeurs arrondies identiques. Note que, pour une meilleure comparaison visuelle, les nombres d'occurrences ont été normalisés de sorte que chaque distribution soit affichée entre 0 et 1.
Comme le montre la figure (ligne bleue continue), la distribution des coûts pour notre approche Variational Quantum Eigensolver (post-traité avec SQD) est fortement concentrée sur des valeurs de coût objectif plus faibles, ce qui indique de bonnes performances d'optimisation. En revanche, la ligne de base bruitée présente une distribution plus large, centrée sur des valeurs de coût plus élevées. La ligne verticale grise en pointillés représente la valeur moyenne de la distribution aléatoire, soulignant davantage la cohérence de la fonction dans le retour de stratégies d'investissement optimisées. Pour une comparaison supplémentaire, la ligne noire en pointillés dans la figure correspond à la solution obtenue avec l'optimiseur Gurobi (version gratuite). Tous ces résultats sont explorés plus en détail dans les benchmarks ci-dessous pour l'exemple « Mixed Assets » évalué avec l'ansatz « Tailored ».
Benchmarks
Cette fonction a été testée sous différentes configurations de qubits de résolution, de circuits ansatz et de regroupements d'actifs issus de divers secteurs : un mélange d'actifs différents (Ensemble 1), des dérivés pétroliers (Ensemble 2) et l'IBEX35 (Ensemble 3). Voir plus de détails dans le tableau suivant.
| Ensemble | Date | Actifs |
|---|---|---|
| Ensemble 1 | 01/01/2023 | 8801.T, CL=F, GBPJPY=X, ITX.MC, META, TMBMKDE-10Y, XS2239553048 |
| Ensemble 2 | 01/06/2023 | CL=F, BZ=F, HO=F, NG=F, XOM, RB=F, 2222.SR |
| Ensemble 3 | 01/11/2022 | ACS.MC, ITX.MC, FER.MC, ELE.MC, SCYR.MC, AENA.MC, AMS.MC |
Deux métriques clés ont été utilisées pour évaluer la qualité des solutions.
- Le coût objectif, qui mesure l'efficacité de l'optimisation en comparant la valeur de la fonction de coût de chaque expérience aux résultats de Gurobi (version gratuite).
- Le ratio de Sharpe, qui capture le rendement ajusté au risque de chaque portefeuille, offrant un aperçu des performances financières des solutions.
Ensemble, ces métriques évaluent les aspects computationnels et financiers des portefeuilles générés par le quantique.
| Exemple | Qubits | Ansatz | Profondeur | Utilisation Runtime (s) | Utilisation totale (s) | Coût objectif | Sharpe | Coût objectif Gurobi | Sharpe Gurobi |
|---|---|---|---|---|---|---|---|---|---|
| Mixed Assets (Ensemble 1, 4 pas, 4-bit) | 112 | Tailored | 83 | 12735 | 13095 | -3.78 | 24.82 | -4.25 | 24.71 |
| Mixed Assets (Ensemble 1, 4 pas, 4-bit) | 112 | Real Amplitudes | 359 | 11739 | 11903 | -3.39 | 23.64 | -4.25 | 24.71 |
| Oil Derivatives (Ensemble 2, 4 pas, 3-bit) | 84 | Optimized Real Amplitudes | 78 | 6180 | 6350 | -3.73 | 19.13 | -4.19 | 21.71 |
| IBEX35 (Ensemble 3, 4 pas, 2-bit) | 56 | Optimized Real Amplitudes | 96 | 3314 | 3523 | -3.67 | 14.48 | -4.11 | 16.44 |
Les résultats montrent que l'optimiseur quantique, avec des ansatzes spécifiques au problème, identifie efficacement des stratégies d'investissement performantes pour différents types de portefeuilles.
Nous détaillons ci-dessous la taille de population et le nombre de générations spécifiés dans le dictionnaire optimizer_options. Tous les autres paramètres ont été laissés à leurs valeurs par défaut.
| Exemple | population_size | num_generations |
|---|---|---|
| Portefeuille Mixed Assets | 90 | 20 |
| Portefeuille Mixed Assets | 92 | 20 |
| Portefeuille Oil Derivatives | 120 | 20 |
| Portefeuille IBEX35 | 40 | 20 |
Le nombre de générations a été fixé à 20, cette valeur s'étant avérée suffisante pour atteindre la convergence. De plus, les valeurs par défaut des paramètres internes de l'optimiseur ont été conservées telles quelles, car elles fournissent systématiquement de bonnes performances et sont généralement recommandées par la littérature et les directives d'implémentation.
Obtenir de l'aide
Si tu as besoin d'aide, tu peux envoyer un e-mail à qpo.support@globaldataquantum.com. Dans ton message, indique l'identifiant du job de la fonction.
Étapes suivantes
- Lis l'article de recherche associé.
-
- Visite la référence API pour cette Qiskit Function.
- Demande l'accès à la fonction en remplissant ce formulaire.
- Essaie le tutoriel Optimisation dynamique de portefeuille.