Zum Hauptinhalt springen

Dynamische Portfolio-Optimierung mit dem Portfolio Optimizer von Global Data Quantum

Hinweis

Qiskit Functions sind ein experimentelles Feature, das ausschließlich Nutzenden des IBM Quantum® Premium Plan, Flex Plan und On-Prem (über die IBM Quantum Platform API) zur Verfügung steht. Sie befinden sich im Preview-Status und können sich ändern.

Geschätzter Aufwand: Ungefähr 55 Minuten auf einem Heron-r2-Prozessor. (HINWEIS: Dies ist nur eine Schätzung. Die tatsächliche Laufzeit kann abweichen.)

Hintergrund

Das dynamische Portfolio-Optimierungsproblem zielt darauf ab, die optimale Anlagestrategie über mehrere Zeiträume hinweg zu finden, um die erwartete Portfolio-Rendite zu maximieren und Risiken zu minimieren – oft unter bestimmten Einschränkungen wie Budget, Transaktionskosten oder Risikoaversion. Im Gegensatz zur Standard-Portfolio-Optimierung, die einen einzelnen Zeitpunkt zur Neugewichtung des Portfolios betrachtet, berücksichtigt die dynamische Variante die sich verändernde Natur der Vermögenswerte und passt Investitionen basierend auf Veränderungen der Asset-Performance im Zeitverlauf an.

Dieses Tutorial zeigt, wie du dynamische Portfolio-Optimierung mithilfe der Qiskit-Funktion Quantum Portfolio Optimizer durchführst. Konkret illustrieren wir, wie du diese Anwendungsfunktion verwendest, um ein Investitionsallokationsproblem über mehrere Zeitschritte hinweg zu lösen.

Der Ansatz beinhaltet die Formulierung der Portfolio-Optimierung als multi-objektives Problem der Quadratischen Uneingeschränkten Binären Optimierung (QUBO). Konkret formulieren wir die QUBO-Funktion OO so, dass sie vier verschiedene Ziele gleichzeitig optimiert:

  • Maximierung der Renditefunktion FF
  • Minimierung des Anlagerisikos RR
  • Minimierung der Transaktionskosten CC
  • Einhaltung der Investitionsbeschränkungen, formuliert als zusätzlichen zu minimierenden Term PP.

Zusammenfassend formulieren wir die QUBO-Funktion zur Bewältigung dieser Ziele als O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, wobei γ\gamma der Risikoaversionskoeffizient und ρ\rho der Verstärkungskoeffizient für Restriktionen (Lagrange-Multiplikator) ist. Die explizite Formulierung findet sich in Gl. (15) unseres Manuskripts [1].

Wir lösen das Problem mit einer hybriden Quanten-Klassik-Methode basierend auf dem Variational Quantum Eigensolver (VQE). In diesem Aufbau schätzt der Quantum Circuit die Kostenfunktion ab, während die klassische Optimierung mit dem Differential-Evolution-Algorithmus durchgeführt wird, was eine effiziente Navigation der Lösungslandschaft ermöglicht. Die Anzahl der benötigten Qubits hängt von drei Hauptfaktoren ab: der Anzahl der Vermögenswerte na, der Anzahl der Zeiträume nt und der Bit-Auflösung zur Darstellung der Investition nq. Konkret beträgt die Mindestanzahl an Qubits in unserem Problem na*nt*nq.

Für dieses Tutorial konzentrieren wir uns auf die Optimierung eines regionalen Portfolios basierend auf dem spanischen IBEX-35-Index. Konkret verwenden wir ein Sieben-Asset-Portfolio, wie in der folgenden Tabelle angegeben:

IBEX-35-PortfolioACS.MCITX.MCFER.MCELE.MCSCYR.MCAENA.MCAMS.MC

Wir gewichten unser Portfolio in vier Zeitschritten neu, jeweils im Abstand von 30 Tagen, beginnend am 1. November 2022. Jede Investitionsvariable wird mit zwei Bits kodiert. Dies ergibt ein Problem, das 56 Qubits zur Lösung benötigt.

Wir verwenden den Optimized Real Amplitudes Ansatz, eine angepasste und hardware-effiziente Adaption des Standard-Real-Amplitudes-Ansatzes, der speziell für eine bessere Performance bei dieser Art von finanziellen Optimierungsproblemen entwickelt wurde.

Die Quantenausführung erfolgt auf dem Backend ibm_torino. Eine detaillierte Erklärung der Problemformulierung, Methodik und Leistungsbewertung findest du im 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 den Quantum Portfolio Optimizer zu verwenden, wähle die Funktion über den Qiskit Functions Catalog aus. Du benötigst ein IBM Quantum Premium Plan oder Flex Plan-Konto sowie eine Lizenz von Global Data Quantum, um diese Funktion auszuführen.

Authentifiziere dich zunächst mit deinem API-Schlüssel. Lade dann die gewünschte Funktion aus dem Qiskit Functions Catalog. Hier greifst du auf die Funktion quantum_portfolio_optimizer aus dem Catalog zu, indem du die Klasse QiskitFunctionsCatalog verwendest. Diese Funktion erlaubt uns, den 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: Eingabe-Portfolio einlesen

In diesem Schritt laden wir historische Daten für die sieben ausgewählten Vermögenswerte aus dem IBEX-35-Index, konkret vom 1. November 2022 bis zum 1. April 2023.

Wir rufen die Daten über die Yahoo Finance API ab und konzentrieren uns dabei auf Schlusskurse. Die Daten werden dann verarbeitet, um sicherzustellen, dass alle Vermögenswerte dieselbe Anzahl von Tagen mit Daten haben. Fehlende Daten (handelsfreie Tage) werden angemessen behandelt, sodass alle Vermögenswerte auf denselben Daten ausgerichtet sind.

Die Daten werden in einem DataFrame mit einheitlicher Formatierung über alle Vermögenswerte hinweg 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: Problemparameter definieren

Die Parameter zur Definition des QUBO-Problems werden im Dictionary qubo_settings konfiguriert. Wir legen die Anzahl der Zeitschritte (nt), die Anzahl der Bits zur Investitionsspezifikation (nq) und das Zeitfenster für jeden Zeitschritt (dt) fest. Außerdem setzen wir die maximale Investition pro Vermögenswert, den Risikoaversionskoeffizienten, die Transaktionsgebühr und den Restriktionskoeffizienten (siehe unser Paper für Details zur Problemformulierung). Diese Einstellungen ermöglichen es uns, das QUBO-Problem an das spezifische Investitionsszenario 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,
}

Das Dictionary optimizer_settings konfiguriert den Optimierungsprozess, einschließlich Parameter wie num_generations für die Anzahl der Iterationen und population_size für die Anzahl der Kandidatenlösungen pro Generation. Weitere Einstellungen steuern Aspekte wie die Rekombinationsrate, parallele Jobs, Batch-Größe und den Mutationsbereich. Darüber hinaus legen die Primitive-Einstellungen wie estimator_shots, estimator_precision und sampler_shots die Quantenschätzer- und Sampler-Konfigurationen für den Optimierungsprozess fest.

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

Die Gesamtzahl der Circuits hängt von den Parametern in optimizer_settings ab und wird als (num_generations + 1) * population_size berechnet.

Das Dictionary ansatz_settings konfiguriert den Ansatz des Quantum Circuits. Der Parameter ansatz gibt die Verwendung des Ansatzes "optimized_real_amplitudes" an, der ein hardware-effizienter Ansatz für finanzielle Optimierungsprobleme ist. Darüber hinaus ist die Einstellung multiple_passmanager aktiviert, um mehrere Pass Manager (einschließlich des standardmäßigen lokalen Qiskit-Pass-Managers und des KI-gestützten Qiskit-Transpiler-Diensts) während des Optimierungsprozesses zu ermöglichen, was die Gesamtperformance und Effizienz der Circuit-Ausführung verbessert.

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

Abschließend führen wir die Optimierung aus, indem wir die Funktion dpo_solver.run() mit den vorbereiteten Eingaben aufrufen. Dazu gehören das Asset-Daten-Dictionary (assets), die QUBO-Konfiguration (qubo_settings), die Optimierungsparameter (optimizer_settings) und die Ansatz-Einstellungen des Quantum Circuits (ansatz_settings). Zusätzlich geben wir die Ausführungsdetails an, wie das Backend und ob eine Nachbearbeitung der Ergebnisse erfolgen soll. Damit wird der dynamische Portfolio-Optimierungsprozess auf dem ausgewählten Quanten-Backend gestartet.

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: Optimierungsergebnisse analysieren

In diesem Abschnitt extrahieren und zeigen wir die Lösung mit den niedrigsten Gesamtkosten aus den Optimierungsergebnissen. Zusammen mit den minimalen Zielkosten präsentieren wir auch wichtige Kennzahlen der zugehörigen Lösung, darunter die Restriktionsabweichung, die Sharpe Ratio und die Investitionsrendite.

# 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

Der folgende Code zeigt, wie die Kostenverteilung eines Optimierungsalgorithmus mit einer Zufallsstichprobenverteilung visualisiert und verglichen werden kann. Wir erkunden auch die Landschaft der QUBO-Zielfunktion (die aus dem Funktionsoutput geladen werden kann), indem wir sie mit zufälligen Investitionen auswerten. Wir stellen beide Verteilungen amplitudennormiert dar, um leichter vergleichen zu können, wie sich der Optimierungsprozess von der Zufallsstichprobe in Bezug auf die Kosten unterscheidet. Außerdem wird das mit DOCPlex erzielte Ergebnis als gestrichelte vertikale Referenzlinie als klassischer Benchmark eingefügt. Wir verwenden die kostenlose Version von DOCPlex – die IBM® Open-Source-Bibliothek für mathematische Optimierung in Python – um dasselbe 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)

Output of the previous code cell

Das Diagramm zeigt, wie der Quantum Portfolio Optimizer konsistent optimierte Anlagestrategien liefert.

Referenzen

[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).

Tutorial-Umfrage

Nimm dir bitte eine Minute Zeit, um Feedback zu diesem Tutorial zu geben. Deine Einblicke helfen uns, unser Inhaltsangebot und die Nutzererfahrung zu verbessern. Link zur Umfrage