Sari la conținutul principal

Efectuează optimizarea dinamică a portofoliului cu Portfolio Optimizer de la Global Data Quantum

Notă

Funcțiile Qiskit sunt o caracteristică experimentală disponibilă doar utilizatorilor IBM Quantum® Premium Plan, Flex Plan și On-Prem (prin API-ul IBM Quantum Platform). Acestea sunt în stadiu de previzualizare și pot fi modificate.

Estimare de utilizare: Aproximativ 55 de minute pe un procesor Heron r2. (NOTĂ: Aceasta este doar o estimare. Timpul real de execuție poate varia.)

Fundal

Problema de optimizare dinamică a portofoliului urmărește găsirea strategiei optime de investiții pe mai multe perioade de timp, pentru a maximiza randamentul așteptat al portofoliului și a minimiza riscurile, adesea sub anumite constrângeri precum buget, costuri de tranzacție sau aversiunea față de risc. Spre deosebire de optimizarea standard a portofoliului, care consideră un singur moment pentru reechilibrare, versiunea dinamică ține cont de natura evolutivă a activelor și adaptează investițiile în funcție de schimbările în performanța activelor în timp.

Acest tutorial demonstrează cum să efectuezi optimizarea dinamică a portofoliului folosind Funcția Qiskit Quantum Portfolio Optimizer. Mai precis, ilustrăm cum să folosești această funcție de aplicație pentru a rezolva o problemă de alocare a investițiilor pe mai multe intervale de timp.

Abordarea implică formularea optimizării portofoliului ca o problemă multi-obiectiv de Optimizare Binară Pătratică Neconstrânsă (QUBO). Mai exact, formulăm funcția QUBO OO pentru a optimiza simultan patru obiective diferite:

  • Maximizarea funcției de randament FF
  • Minimizarea riscului investiției RR
  • Minimizarea costurilor de tranzacție CC
  • Respectarea restricțiilor de investiție, formulate într-un termen suplimentar de minimizat PP.

Pe scurt, pentru a aborda aceste obiective, formulăm funcția QUBO astfel O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, unde γ\gamma este coeficientul de aversiune față de risc și ρ\rho este coeficientul de întărire a restricțiilor (multiplicatorul Lagrange). Formularea explicită poate fi găsită în Eq. (15) din manuscrisul nostru [1].

Rezolvăm problema folosind o metodă hibridă cuantică-clasică bazată pe Variational Quantum Eigensolver (VQE). În această configurație, circuitul cuantic estimează funcția de cost, în timp ce optimizarea clasică se realizează folosind algoritmul Differential Evolution, permițând navigarea eficientă a spațiului de soluții. Numărul de qubiți necesari depinde de trei factori principali: numărul de active na, numărul de perioade de timp nt și rezoluția în biți folosită pentru a reprezenta investiția nq. Mai precis, numărul minim de qubiți din problema noastră este na*nt*nq.

Pentru acest tutorial, ne concentrăm pe optimizarea unui portofoliu regional bazat pe indicele spaniol IBEX 35. Mai precis, folosim un portofoliu cu șapte active, după cum se indică în tabelul de mai jos:

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

Ne reechilibrăm portofoliul în patru intervale de timp, fiecare separat printr-un interval de 30 de zile, începând cu 1 noiembrie 2022. Fiecare variabilă de investiție este codificată folosind doi biți. Aceasta rezultă într-o problemă care necesită 56 de qubiți pentru a fi rezolvată.

Folosim ansatz-ul Optimized Real Amplitudes, o adaptare personalizată și eficientă din punct de vedere hardware a ansatz-ului standard Real Amplitudes, special concepută pentru a îmbunătăți performanța pentru acest tip de problemă de optimizare financiară.

Execuția cuantică se efectuează pe backend-ul ibm_torino. Pentru o explicație detaliată a formulării problemei, metodologiei și evaluării performanței, consultă manuscrisul publicat [1].

Cerințe

# 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

Configurare

Pentru a folosi Quantum Portfolio Optimizer, selectează funcția prin Catalogul de Funcții Qiskit. Ai nevoie de un cont IBM Quantum Premium Plan sau Flex Plan cu o licență de la Global Data Quantum pentru a rula această funcție.

Mai întâi, autentifică-te cu cheia ta API. Apoi, încarcă funcția dorită din Catalogul de Funcții Qiskit. Aici, accesezi funcția quantum_portfolio_optimizer din catalog folosind clasa QiskitFunctionsCatalog. Această funcție ne permite să utilizăm solver-ul predefinit 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")

Pasul 1: Citirea portofoliului de intrare

În acest pas, încărcăm date istorice pentru cele șapte active selectate din indicele IBEX 35, mai precis din 1 noiembrie 2022 până în 1 aprilie 2023.

Preluăm datele folosind API-ul Yahoo Finance, concentrându-ne pe prețurile de închidere. Datele sunt apoi procesate pentru a ne asigura că toate activele au același număr de zile cu date. Orice date lipsă (zile fără tranzacționare) sunt gestionate corespunzător, asigurând alinierea tuturor activelor pe aceleași date.

Datele sunt structurate într-un DataFrame cu formatare consistentă pentru toate activele.

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

Pasul 2: Definirea parametrilor problemei

Parametrii necesari pentru a defini problema QUBO sunt configurați în dicționarul qubo_settings. Definim numărul de intervale de timp (nt), numărul de biți pentru specificarea investiției (nq) și fereastra de timp pentru fiecare interval de timp (dt). În plus, setăm investiția maximă per activ, coeficientul de aversiune față de risc, comisionul de tranzacție și coeficientul de restricție (consultă lucrarea noastră pentru detalii despre formularea problemei). Aceste setări ne permit să adaptăm problema QUBO la scenariul specific de investiții.

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

Dicționarul optimizer_settings configurează procesul de optimizare, incluzând parametri precum num_generations pentru numărul de iterații și population_size pentru numărul de soluții candidate per generație. Alte setări controlează aspecte precum rata de recombiare, joburile paralele, dimensiunea lotului și intervalul de mutație. În plus, setările pentru primitive, cum ar fi estimator_shots, estimator_precision și sampler_shots, definesc configurațiile cuantice ale Estimator-ului și Sampler-ului pentru procesul de optimizare.

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

Numărul total de circuite depinde de parametrii optimizer_settings și se calculează ca (num_generations + 1) * population_size.

Dicționarul ansatz_settings configurează ansatz-ul circuitului cuantic. Parametrul ansatz specifică utilizarea abordării "optimized_real_amplitudes", care este un ansatz eficient din punct de vedere hardware, conceput pentru probleme de optimizare financiară. În plus, setarea multiple_passmanager este activată pentru a permite mai multe managere de trecere (inclusiv managerul local implicit Qiskit și serviciul de transpilare alimentat de Qiskit AI) în timpul procesului de optimizare, îmbunătățind performanța generală și eficiența execuției circuitului.

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

În cele din urmă, executăm optimizarea rulând funcția dpo_solver.run(), transmițând datele de intrare pregătite. Acestea includ dicționarul cu datele activelor (assets), configurația QUBO (qubo_settings), parametrii de optimizare (optimizer_settings) și setările ansatz-ului circuitului cuantic (ansatz_settings). În plus, specificăm detaliile de execuție, cum ar fi backend-ul, și dacă să se aplice post-procesarea rezultatelor. Aceasta inițiază procesul de optimizare dinamică a portofoliului pe backend-ul cuantic selectat.

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

Pasul 3: Analizează rezultatele optimizării

În această secțiune, extragem și afișăm soluția cu cel mai mic cost obiectiv din rezultatele optimizării. Alături de costul obiectiv minim, prezentăm și metrici cheie asociate soluției respective, inclusiv deviația restricțiilor, raportul Sharpe și randamentul investiției.

# 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

Codul următor arată cum să vizualizezi și să compari distribuția costurilor unui algoritm de optimizare cu o distribuție de eșantionare aleatorie. În mod similar, explorăm peisajul funcției obiectiv QUBO (care poate fi încărcată din rezultatul funcției) evaluând-o cu investiții aleatorii. Reprezentăm ambele distribuții normalizate în amplitudine, pentru a compara mai ușor modul în care procesul de optimizare diferă de eșantionarea aleatorie în termeni de cost. În plus, rezultatul obținut cu DOCPlex este inclus ca o linie verticală punctată de referință, servind drept reper clasic. Folosim versiunea gratuită a DOCPlex — biblioteca open-source IBM® pentru optimizare matematică în Python — pentru a rezolva aceeași problemă în mod clasic.

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

Graficul arată că optimizatorul cuantic de portofoliu returnează în mod constant strategii de investiții optimizate.

Referințe

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

Sondaj tutorial

Te rog să aloci un minut pentru a oferi feedback despre acest tutorial. Opiniile tale ne vor ajuta să îmbunătățim conținutul și experiența utilizatorilor. Link spre sondaj

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.