Przejdź do głównej treści

Przeprowadź dynamiczną optymalizację portfela za pomocą Portfolio Optimizer od Global Data Quantum

Uwaga

Funkcje Qiskit to funkcja eksperymentalna dostępna wyłącznie dla użytkowników planów IBM Quantum® Premium Plan, Flex Plan i On-Prem (za pośrednictwem API IBM Quantum Platform). Są w fazie podglądu i mogą ulec zmianie.

Szacowany czas użytkowania: Około 55 minut na procesorze Heron r2. (UWAGA: To jest tylko szacunek. Rzeczywisty czas działania może się różnić.)

Tło

Problem dynamicznej optymalizacji portfela polega na znalezieniu optymalnej strategii inwestycyjnej w wielu okresach czasu, aby zmaksymalizować oczekiwany zwrot z portfela i zminimalizować ryzyko, często przy określonych ograniczeniach, takich jak budżet, koszty transakcji lub awersja do ryzyka. W przeciwieństwie do standardowej optymalizacji portfela, która rozważa jeden moment rebalansowania portfela, wersja dynamiczna uwzględnia ewoluujący charakter aktywów i dostosowuje inwestycje na podstawie zmian wyników aktywów w czasie.

Ten samouczek pokazuje, jak przeprowadzić dynamiczną optymalizację portfela za pomocą funkcji Qiskit Quantum Portfolio Optimizer. Konkretnie ilustrujemy, jak używać tej funkcji aplikacyjnej do rozwiązania problemu alokacji inwestycji w wielu krokach czasowych.

Podejście polega na sformułowaniu optymalizacji portfela jako wielocelowego problemu kwadratowej nieograniczonej optymalizacji binarnej (QUBO). Konkretnie formułujemy funkcję QUBO OO w celu jednoczesnej optymalizacji czterech różnych celów:

  • Maksymalizacja funkcji zwrotu FF
  • Minimalizacja ryzyka inwestycji RR
  • Minimalizacja kosztów transakcji CC
  • Spełnienie ograniczeń inwestycyjnych, sformułowanych jako dodatkowy termin do minimalizacji PP.

Podsumowując, aby zająć się tymi celami, formułujemy funkcję QUBO jako O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, gdzie γ\gamma jest współczynnikiem awersji do ryzyka, a ρ\rho jest współczynnikiem wzmocnienia ograniczeń (mnożnik Lagrange'a). Jawne sformułowanie można znaleźć w równaniu (15) naszego manuskryptu [1].

Rozwiązujemy problem za pomocą hybrydowej metody kwantowo-klasycznej opartej na wariacyjnym rozwiązywaczu własnym (VQE). W tej konfiguracji Circuit kwantowy szacuje funkcję kosztu, podczas gdy klasyczna optymalizacja jest wykonywana za pomocą algorytmu ewolucji różnicowej, umożliwiając efektywne poruszanie się po przestrzeni rozwiązań. Liczba Qubitów zależy od trzech głównych czynników: liczby aktywów na, liczby okresów czasowych nt i rozdzielczości bitowej używanej do reprezentowania inwestycji nq. Konkretnie, minimalna liczba Qubitów w naszym problemie wynosi na*nt*nq.

W tym samouczku skupiamy się na optymalizacji regionalnego portfela opartego na indeksie IBEX 35 z Hiszpanii. Konkretnie używamy portfela siedmiu aktywów wskazanych w poniższej tabeli:

Portfel IBEX 35ACS.MCITX.MCFER.MCELE.MCSCYR.MCAENA.MCAMS.MC

Rebalansujemy nasz portfel w czterech krokach czasowych, z których każdy jest oddzielony 30-dniowym interwałem, zaczynając od 1 listopada 2022 r. Każda zmienna inwestycyjna jest kodowana przy użyciu dwóch bitów. Daje to problem wymagający 56 Qubitów do rozwiązania.

Używamy ansatzu Optimized Real Amplitudes, dostosowanej i wydajnej sprzętowo adaptacji standardowego ansatzu Real Amplitudes, specjalnie dostosowanej do poprawy wydajności dla tego rodzaju problemów optymalizacji finansowej.

Wykonanie kwantowe jest przeprowadzane na Backend ibm_torino. Szczegółowe wyjaśnienie sformułowania problemu, metodologii i oceny wydajności można znaleźć w opublikowanym manuskrypcie [1].

Wymagania

# 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

Konfiguracja

Aby używać Quantum Portfolio Optimizer, wybierz funkcję z katalogu Qiskit Functions Catalog. Potrzebujesz konta IBM Quantum Premium Plan lub Flex Plan z licencją od Global Data Quantum, aby uruchomić tę funkcję.

Najpierw uwierzytelnij się za pomocą klucza API. Następnie załaduj żądaną funkcję z katalogu Qiskit Functions Catalog. Tutaj uzyskujesz dostęp do funkcji quantum_portfolio_optimizer z katalogu za pomocą klasy QiskitFunctionsCatalog. Ta funkcja pozwala nam używać predefiniowanego solwera Quantum Portfolio Optimization.

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

Krok 1: Wczytaj portfel wejściowy

W tym kroku ładujemy dane historyczne dla siedmiu wybranych aktywów z indeksu IBEX 35, konkretnie od 1 listopada 2022 do 1 kwietnia 2023.

Pobieramy dane za pomocą API Yahoo Finance, skupiając się na cenach zamknięcia. Dane są następnie przetwarzane, aby zapewnić, że wszystkie aktywa mają taką samą liczbę dni z danymi. Wszelkie brakujące dane (dni nie będące dniami handlowymi) są odpowiednio obsługiwane, zapewniając wyrównanie wszystkich aktywów na tych samych datach.

Dane są ustrukturyzowane w DataFrame z zachowaniem spójnego formatowania dla wszystkich aktywów.

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

Krok 2: Zdefiniuj dane wejściowe problemu

Parametry potrzebne do zdefiniowania problemu QUBO są konfigurowane w słowniku qubo_settings. Definiujemy liczbę kroków czasowych (nt), liczbę bitów do specyfikacji inwestycji (nq) i okno czasowe dla każdego kroku czasowego (dt). Ponadto ustawiamy maksymalną inwestycję na aktywo, współczynnik awersji do ryzyka, opłatę transakcyjną i współczynnik ograniczeń (szczegóły dotyczące sformułowania problemu można znaleźć w naszym artykule). Te ustawienia pozwalają nam dostosować problem QUBO do konkretnego scenariusza inwestycyjnego.

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

Słownik optimizer_settings konfiguruje proces optymalizacji, w tym parametry takie jak num_generations dla liczby iteracji i population_size dla liczby kandydatów na rozwiązania w każdym pokoleniu. Inne ustawienia kontrolują takie aspekty jak współczynnik rekombinacji, równoległe zadania, rozmiar wsadu i zakres mutacji. Ponadto ustawienia prymitywne, takie jak estimator_shots, estimator_precision i sampler_shots, definiują konfiguracje Estimatora kwantowego i Samplera dla procesu optymalizacji.

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

Całkowita liczba układów zależy od parametrów optimizer_settings i jest obliczana jako (num_generations + 1) * population_size.

Słownik ansatz_settings konfiguruje ansatz Circuit kwantowego. Parametr ansatz określa użycie podejścia "optimized_real_amplitudes", które jest wydajnym sprzętowo ansatzem zaprojektowanym dla problemów optymalizacji finansowej. Ponadto ustawienie multiple_passmanager jest włączone, aby umożliwić wiele menedżerów przejść (w tym domyślny lokalny menedżer przejść Qiskit i zasilaną przez AI usługę Transpilera Qiskit) podczas procesu optymalizacji, poprawiając ogólną wydajność i efektywność wykonywania Circuit.

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

Na koniec wykonujemy optymalizację, uruchamiając funkcję dpo_solver.run() i przekazując przygotowane dane wejściowe. Obejmują one słownik danych aktywów (assets), konfigurację QUBO (qubo_settings), parametry optymalizacji (optimizer_settings) i ustawienia ansatzu Circuit kwantowego (ansatz_settings). Ponadto określamy szczegóły wykonania, takie jak Backend, i czy zastosować przetwarzanie końcowe wyników. Inicjuje to proces dynamicznej optymalizacji portfela na wybranym Backend kwantowym.

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

Krok 3: Analiza wyników optymalizacji

W tej sekcji wyodrębniamy i wyświetlamy rozwiązanie o najniższym koszcie funkcji celu spośród wyników optymalizacji. Oprócz minimalnego kosztu funkcji celu prezentujemy również kluczowe metryki powiązane z tym rozwiązaniem, w tym odchylenie od ograniczeń, wskaźnik Sharpe'a oraz zwrot z inwestycji.

# 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

Poniższy kod pokazuje, jak zwizualizować i porównać rozkład kosztów algorytmu optymalizacji z rozkładem losowego próbkowania. W podobny sposób badamy krajobraz funkcji celu QUBO (którą można wczytać z wyników funkcji), oceniając ją dla losowych inwestycji. Oba rozkłady wykreślamy znormalizowane amplitudowo, aby łatwiej porównać, czym proces optymalizacji różni się od losowego próbkowania pod względem kosztu. Dodatkowo wynik uzyskany za pomocą DOCPlex jest zaznaczony jako przerywana pionowa linia referencyjna, stanowiąca klasyczny punkt odniesienia. Używamy bezpłatnej wersji DOCPlex — open-source'owej biblioteki IBM® do optymalizacji matematycznej w Pythonie — aby rozwiązać ten sam problem klasycznie.

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

Wykres pokazuje, że kwantowy optymalizator portfela konsekwentnie zwraca zoptymalizowane strategie inwestycyjne.

Odniesienia

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

Ankieta dotycząca samouczka

Poświęć chwilę na przesłanie opinii na temat tego samouczka. Twoje uwagi pomogą nam ulepszyć nasze materiały i doświadczenie użytkownika. Link do ankiety

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.