Machet dynamische Portfolio-Optimierung mit'n Portfolio Optimizer von Global Data Quantum
Qiskit Functions sind ne experimentelle Feature und nur für IBM Quantum® Premium Plan, Flex Plan und On-Prem (übern IBM Quantum Platform API) Plan-Nutzer verfügbar. Se sind im Preview-Status und könn' sich noch ändern.
Verbrauchs-Schätzung: Unjeföhr 55 Minuten uff'n Heron r2-Prozessor. (Uffjepasst: Det is nur ne Schätzung. Die wirkliche Zeit kann anders ausfallen.)
Hintergrund
Det dynamische Portfolio-Optimierungs-Problem hat det Ziel, die optimale Investitions-Strategie über mehrere Zeiten zu finden, um de erwartete Rendite vom Portfolio zu maximieren und de Risiken zu minimieren — oft unter bestimmten Bedingungen wie Budget, Transaktions-Kosten oder Risiko-Aversion. Anders als die standard Portfolio-Optimierung, die nur eenen Moment für'det Rebalancieren vom Portfolio betrachtet, berücksichtigt die dynamische Version die Änderungen vom Wert von de Assets und passt die Investitionen jenau an, wie sich die Performance von de Assets über die Zeit verändert.
Det Tutorial hier zeigt, wie wir dynamische Portfolio-Optimierung mit de Quantum Portfolio Optimizer Qiskit Function machen könn'. Speziell zeigen wir, wie wir disse Application-Function nutzen könn', um een Investitions-Verteilungs-Problem über mehrere Zeit-Schritte zu lösen.
De Ansatz formuliert die Portfolio-Optimierung als een Multi-Objective Quadratic Unconstrained Binary Optimization (QUBO) Problem. Speziell formulieren wir die QUBO-Funktion so, dat se vier verschiedene Zielsetzungen jleichzeitich optimiert:
- Maximieren die Rendite-Funktion
- Minimieren det Risiko von de Investitionen
- Minimieren die Transaktions-Kosten
- Haltet euch an die Investitions-Beschränkungen, formuliert in eenen zusätzlichen Term zum Minimieren von .
Zusammjefasst formulieren wir die QUBO-Funktion so wo de Risiko-Aversions-Koeffizient is und de Beschränkungs-Verstärkungs-Koeffizient (Lagrange-Multiplikator). Die explizite Formulierung findet ihr in Gl. (15) von unsere Veröffentlichung [1].
Wir lösen det mit ne hybriden Quantum-Klassischen Methode, die uff'm Variational Quantum Eigensolver (VQE) basiert. In det Setup schätzt de Quantenschaltkreis die Kosten-Funktion, während die klassische Optimierung mit'n Differential Evolution-Algorithmus durchjeführt wird, wat et möglich macht, die Lösungs-Landschaft effizient zu durchsuchen. Die Anzahl von Qubits, die wa brauchen, hängt von drei Hauptfaktoren ab: die Anzahl von Assets na, die Anzahl von Zeit-Perioden nt und die Bit-Auflösung für die Darstellung von de Investitionen nq. Konkret is die minimale Anzahl von Qubits in unserm Problem na*nt*nq.
In det Tutorial konzentrieren wa uns uff die Optimierung von eenen regionalen Portfolio, det uff'm spanischen IBEX 35-Index basiert. Speziell brauchen wa een Sieben-Asset-Portfolio wie in de Tabelle hier:
| IBEX 35 Portfolio | ACS.MC | ITX.MC | FER.MC | ELE.MC | SCYR.MC | AENA.MC | AMS.MC |
|---|
Wa rebalancieren unser Portfolio in vier Zeit-Schritte, jeweilsch 30 Tage auseinander, anfangend am 1. November 2022. Jede Investitions-Variable wird mit zweien Bits kodiert. Det führt zu eenen Problem, det 56 Qubits braucht, um't zu lösen.
Wa nutzen de Optimized Real Amplitudes-Ansatz, ne anjepasste und hardware-effiziente Adaptierung vom standard Real Amplitudes-Ansatz, speziell anjepasst, um die Performance für disse Art von finanzieller Optimierung zu verbessern.
Die Quantum-Ausführung wird uff'm ibm_torino-Backend durchjeführt. Für ne detaillierte Erklärung von de Problem-Formulierung, Methodologie und Performance-Evaluierung kuckt nach de veröffentlichten Manuskript [1].
Voraussetzungen
# Added by doQumentation — required packages for this notebook
!pip install -q numpy
!pip install qiskit-ibm-catalog
!pip install pandas
!pip install matplotlib
!pip install yfinance
Einrichtung
Um de Quantum Portfolio Optimizer zu nutzen, wählt det Function-Objekt über'n Qiskit Functions Catalog aus. Ihr braucht een IBM Quantum Premium Plan- oder Flex Plan-Konto mit ne Lizenz von Global Data Quantum, um disse Function zu nutzen.
Zuerst authentifiziert euch mit euerm API-Schlüssel. Dann laadt det jewünschte Function-Objekt aus'm Qiskit Functions Catalog. Hier greift ihr uff die quantum_portfolio_optimizer-Function aus'm Catalog zu, indem ihr die QiskitFunctionsCatalog-Klasse nutzt. Disse Function erlaubt et uns, de vordefinierten Quantum Portfolio Optimization-Solver zu nutzen.
from qiskit_ibm_catalog import QiskitFunctionsCatalog
catalog = QiskitFunctionsCatalog(
channel="ibm_quantum_platform",
instance="INSTANCE_CRN",
token="YOUR_API_KEY", # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard
)
# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")
Schritt 1: Leset det Input-Portfolio
In det Schritt laden wa historische Daten für die sieben ausgewählten Assets aus'm IBEX 35-Index, speziell vom 1. November 2022 bis 1. April 2023.
Wa holen die Daten über die Yahoo Finance-API und konzentrieren uns uff die Schlusskurse. Die Daten werden dann so bearbeitet, dat alle Assets die jleiche Anzahl von Tagen mit Daten haben. Fehlende Daten (Nich-Handels-Tage) werden passend behandelt, so dat alle Assets uff die jleichen Daten ausjerichtet sind.
Die Daten sind in eenen DataFrame mit konsistenter Formatierung für alle Assets strukturiert.
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
df = pd.concat(series_list, axis=1)
# Convert index to string for consistency
df.index = df.index.astype(str)
# Convert DataFrame to dictionary
assets = df.to_dict()
[*********************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...
Schritt 2: Definiert die Problem-Eingaben
Die Parameter, die wa brauchen, um det QUBO-Problem zu definieren, werden im qubo_settings-Dictionary konfiguriert. Wa definieren die Anzahl von Zeit-Schritten (nt), die Anzahl von Bits für die Investitions-Spezifikation (nq) und det Zeit-Fenster für jeden Zeit-Schritt (dt). Zusätzlich setzen wa die maximale Investitionen pro Asset, de Risiko-Aversions-Koeffizienten, die Transaktions-Jebühr und de Beschränkungs-Koeffizienten (kuckt nach unserem Paper für Details über die Problem-Formulierung). Disse Einstellungen erlauben et uns, det QUBO-Problem an det spezielle Investitions-Szenario anzupassen.
qubo_settings = {
"nt": 4,
"nq": 2,
"dt": 30,
"max_investment": 5, # maximum investment per asset is 2**nq/max_investment = 80%
"risk_aversion": 1000.0,
"transaction_fee": 0.01,
"restriction_coeff": 1.0,
}
Det optimizer_settings-Dictionary konfiguriert de Optimierungs-Prozess, mit Parametern wie num_generations für die Anzahl von Iterationen und population_size für die Anzahl von Kandidaten-Lösungen pro Generation. Andere Einstellungen kontrollieren Aspekte wie die Rekombinations-Rate, parallele Jobs, Batch-Größe und Mutations-Bereich. Zusätzlich definieren die Primitive-Einstellungen wie estimator_shots, estimator_precision und sampler_shots die Quantum-Estimator- und Sampler-Konfigurationen für de Optimierungs-Prozess.
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,
},
}
Die jesamt Anzahl von Schaltkreisen hängt von de optimizer_settings-Parametern ab und wird berechnet als (num_generations + 1) * population_size.
Det ansatz_settings-Dictionary konfiguriert de Quantum-Schaltkreis-Ansatz. De ansatz-Parameter spezifiziert die Nutzung vom "optimized_real_amplitudes"-Ansatz, wat een hardware-effizienter Ansatz is, de für finanzielle Optimierungs-Probleme entwickelt wurde. Zusätzlich is die multiple_passmanager-Einstellung aktiviert, um mehrere Pass-Manager (inklusive de Standard lokal Qiskit Pass-Manager und de Qiskit AI-anjetriebene Transpiler-Service) während'm Optimierungs-Prozess zu erlauben, wat die jesamt Performance und Effizienz von de Schaltkreis-Ausführung verbessert.
ansatz_settings = {
"ansatz": "optimized_real_amplitudes",
"multiple_passmanager": False,
}
Schließlich führen wa die Optimierung aus, indem wa die dpo_solver.run()-Funktion ausführen und die vorbereiteten Eingaben durchjeben. Det umfasst det Asset-Daten-Dictionary (assets), die QUBO-Konfiguration (qubo_settings), Optimierungs-Parameter (optimizer_settings) und die Quantum-Schaltkreis-Ansatz-Einstellungen (ansatz_settings). Zusätzlich spezifizieren wa die Ausführungs-Details wie det Backend und ob wa Post-Processing uff die Resultate anwenden. Det startet de dynamischen Portfolio-Optimierungs-Prozess uff'm ausgewählten Quantum-Backend.
dpo_job = dpo_solver.run(
assets=assets,
qubo_settings=qubo_settings,
optimizer_settings=optimizer_settings,
ansatz_settings=ansatz_settings,
backend_name="ibm_torino",
previous_session_id=[],
apply_postprocess=True,
)
Schritt 3: Analysiert die Optimierungs-Resultate
In det Abschnitt extrahieren wa und zeigen die Lösung mit de niedrigsten objektiven Kosten aus de Optimierungs-Resultate. Neben de minimalen objektiven Kosten präsentieren wa ooch Schlüssel-Metriken, die mit de dazujeherigen Lösung verbunden sind, wie die Beschränkungs-Abweichung, Sharpe-Ratio und Investitions-Rendite.
# Get the results of the job
dpo_result = dpo_job.result()
# Show the solution strategy
dpo_result["result"]
{'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}")
Minimum Objective Cost Found: -3.67
Best Solution:
- Restriction Deviation: 40.0%
- Sharpe Ratio: 14.54
- Return: 0.28
De folgend Code zeigt, wie wa die Kosten-Verteilung von eenen Optimierungs-Algorithmus mit ne zufälligen Stichproben-Verteilung visualisieren und vergleichen könn'. Ähnlich erforschen wa die Landschaft von de QUBO-objektiven Funktion (die aus'm Function-Output jeladen werden kann), indem wa se mit zufälligen Investitionen evaluieren. Wa plotten beide Verteilungen, normalisiert in Amplitude, für eenen einfacheren Vergleich, wie sich de Optimierungs-Prozess von zufälligen Stichproben in Bezug uff Kosten unterscheidet. Zusätzlich wird det Resultat, det wa mit DOCPlex jekricht haben, als jestrichelte vertikale Referenzlinie mitaufjenom, um als klassische Benchmark zu dienen. Wa nutzen die freie Version von DOCPlex — die IBM® Open-Source-Bibliothek für mathematische Optimierung in Python — um det jleiche Problem klassisch zu lösen.
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"][
"objective_costs"
] # List of how many times each solution occurred
cost_list = dpo_result["metadata"]["all_samples_metrics"][
"counts"
] # 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)
De Graph zeigt, wie de Quantum Portfolio Optimizer konsequent optimierte Investitions-Strategien zurückjibt.
Referenzen
Tutorial-Umfrage
Nehmt euch bitte ne Minute Zeit, um Feedback über det Tutorial zu jeben. Eure Einsichten helfen uns, unser Content-Anjebot und User-Experience zu verbessern. Link zur Umfrage