Перейти до основного вмісту

Виконання динамічної оптимізації портфеля за допомогою Оптимізатора портфеля від Global Data Quantum

Примітка

Функції Qiskit — це експериментальна можливість, доступна лише для користувачів планів IBM Quantum® Premium Plan, Flex Plan та On-Prem (через IBM Quantum Platform API). Вони перебувають у статусі попереднього випуску та можуть бути змінені.

Оцінка використання: приблизно 55 хвилин на процесорі Heron r2. (ПРИМІТКА: Це лише оцінка. Фактичний час виконання може відрізнятися.)

Передумови

Задача динамічної оптимізації портфеля спрямована на пошук оптимальної інвестиційної стратегії протягом кількох часових періодів з метою максимізації очікуваної дохідності портфеля та мінімізації ризиків, часто за певних обмежень, таких як бюджет, транзакційні витрати або несхильність до ризику. На відміну від стандартної оптимізації портфеля, яка розглядає один момент часу для ребалансування портфеля, динамічна версія враховує еволюційну природу активів та адаптує інвестиції на основі змін у продуктивності активів з плином часу.

У цьому посібнику показано, як виконати динамічну оптимізацію портфеля за допомогою функції Qiskit «Квантовий оптимізатор портфеля». Зокрема, ми демонструємо, як використовувати цю прикладну функцію для розв'язання задачі розподілу інвестицій протягом кількох часових кроків.

Підхід передбачає формулювання оптимізації портфеля як багатоцільової задачі квадратичної необмеженої бінарної оптимізації (QUBO). Зокрема, ми формулюємо функцію QUBO OO для одночасної оптимізації чотирьох різних цілей:

  • Максимізація функції дохідності FF
  • Мінімізація ризику інвестицій RR
  • Мінімізація транзакційних витрат CC
  • Дотримання інвестиційних обмежень, сформульованих у додатковому члені для мінімізації PP.

Підсумовуючи, для вирішення цих цілей ми формулюємо функцію QUBO як O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, де γ\gamma — коефіцієнт несхильності до ризику, а ρ\rho — коефіцієнт посилення обмежень (множник Лагранжа). Явне формулювання можна знайти у рівнянні (15) нашого рукопису [1].

Ми розв'язуємо задачу за допомогою гібридного квантово-класичного методу на основі варіаційного квантового власного розв'язувача (VQE). У цій схемі квантова схема оцінює функцію вартості, тоді як класична оптимізація виконується за допомогою алгоритму диференціальної еволюції, що забезпечує ефективну навігацію у просторі розв'язків. Кількість необхідних кубітів залежить від трьох основних факторів: кількості активів na, кількості часових періодів nt та бітової роздільної здатності, що використовується для представлення інвестицій nq. Зокрема, мінімальна кількість кубітів у нашій задачі становить na*nt*nq.

У цьому посібнику ми зосереджуємося на оптимізації регіонального портфеля на основі іспанського індексу IBEX 35. Зокрема, ми використовуємо портфель із семи активів, як зазначено в таблиці нижче:

Портфель IBEX 35ACS.MCITX.MCFER.MCELE.MCSCYR.MCAENA.MCAMS.MC

Ми ребалансуємо наш портфель у чотири часові кроки, кожен з яких розділений 30-денним інтервалом, починаючи з 1 листопада 2022 року. Кожна інвестиційна змінна кодується двома бітами. Це призводить до задачі, яка потребує 56 кубітів для розв'язання.

Ми використовуємо ансац оптимізованих дійсних амплітуд — налаштовану та апаратно-ефективну адаптацію стандартного ансацу дійсних амплітуд, спеціально розроблену для покращення продуктивності у цьому типі задач фінансової оптимізації.

Квантове виконання здійснюється на бекенді ibm_torino. Для детального пояснення формулювання задачі, методології та оцінки продуктивності зверніться до опублікованого рукопису [1].

Вимоги

# 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

Налаштування

Щоб використовувати Квантовий оптимізатор портфеля, виберіть функцію через каталог функцій Qiskit. Вам потрібен обліковий запис IBM Quantum Premium Plan або Flex Plan з ліцензією від Global Data Quantum для запуску цієї функції.

Спочатку автентифікуйтеся за допомогою Вашого ключа API. Потім завантажте потрібну функцію з каталогу функцій Qiskit. Тут Ви отримуєте доступ до функції quantum_portfolio_optimizer з каталогу за допомогою класу QiskitFunctionsCatalog. Ця функція дозволяє нам використовувати попередньо визначений розв'язувач квантової оптимізації портфеля.

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

Крок 1: Зчитування вхідного портфеля

На цьому кроці ми завантажуємо історичні дані для семи обраних активів з індексу IBEX 35, зокрема з 1 листопада 2022 року по 1 квітня 2023 року.

Ми отримуємо дані за допомогою API Yahoo Finance, зосереджуючись на цінах закриття. Потім дані обробляються для забезпечення однакової кількості днів з даними для всіх активів. Будь-які відсутні дані (неторговельні дні) обробляються належним чином, забезпечуючи узгодження всіх активів за однаковими датами.

Дані структуровані у DataFrame з послідовним форматуванням для всіх активів.

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

Крок 2: Визначення вхідних параметрів задачі

Параметри, необхідні для визначення задачі QUBO, налаштовуються у словнику qubo_settings. Ми визначаємо кількість часових кроків (nt), кількість бітів для специфікації інвестицій (nq) та часове вікно для кожного часового кроку (dt). Крім того, ми встановлюємо максимальну інвестицію на актив, коефіцієнт несхильності до ризику, комісію за транзакцію та коефіцієнт обмежень (див. нашу статтю для деталей формулювання задачі). Ці налаштування дозволяють нам адаптувати задачу QUBO до конкретного інвестиційного сценарію.

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

Словник optimizer_settings налаштовує процес оптимізації, включаючи такі параметри, як num_generations для кількості ітерацій та population_size для кількості кандидатних розв'язків на покоління. Інші налаштування контролюють такі аспекти, як швидкість рекомбінації, паралельні завдання, розмір пакета та діапазон мутації. Крім того, налаштування примітивів, такі як estimator_shots, estimator_precision та sampler_shots, визначають конфігурації квантового оцінювача та семплера для процесу оптимізації.

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,
},
}
примітка

Загальна кількість схем залежить від параметрів optimizer_settings і обчислюється як (num_generations + 1) * population_size.

Словник ansatz_settings налаштовує ансац квантової схеми. Параметр ansatz вказує на використання підходу "optimized_real_amplitudes", який є апаратно-ефективним ансацем, розробленим для задач фінансової оптимізації. Крім того, налаштування multiple_passmanager увімкнено для використання кількох менеджерів проходів (включаючи стандартний локальний менеджер проходів Qiskit та сервіс транспіляції Qiskit на основі ШІ) під час процесу оптимізації, що покращує загальну продуктивність та ефективність виконання схеми.

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

Нарешті, ми виконуємо оптимізацію, запускаючи функцію dpo_solver.run() та передаючи підготовлені вхідні дані. Вони включають словник даних активів (assets), конфігурацію QUBO (qubo_settings), параметри оптимізації (optimizer_settings) та налаштування ансацу квантової схеми (ansatz_settings). Крім того, ми вказуємо деталі виконання, такі як бекенд, та чи застосовувати постобробку до результатів. Це запускає процес динамічної оптимізації портфеля на обраному квантовому бекенді.

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

Крок 3: Аналіз результатів оптимізації

У цьому розділі ми витягуємо та відображаємо розв'язок із найнижчою цільовою вартістю з результатів оптимізації. Разом із мінімальною цільовою вартістю ми також представляємо ключові метрики, пов'язані з відповідним розв'язком, включаючи відхилення обмежень, коефіцієнт Шарпа та дохідність інвестицій.

# 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

Наступний код показує, як візуалізувати та порівняти розподіл вартості алгоритму оптимізації з розподілом випадкової вибірки. Аналогічно ми досліджуємо ландшафт цільової функції QUBO (яку можна завантажити з виходу функції), оцінюючи її за допомогою випадкових інвестицій. Ми будуємо обидва розподіли, нормалізовані за амплітудою, для зручнішого порівняння того, як процес оптимізації відрізняється від випадкової вибірки з точки зору вартості. Крім того, результат, отриманий за допомогою DOCPlex, включено як пунктирну вертикальну лінію для використання як класичний еталон. Ми використовуємо безкоштовну версію DOCPlex — бібліотеку IBM® з відкритим вихідним кодом для математичної оптимізації на Python — для класичного розв'язання тієї самої задачі.

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

Графік показує, як квантовий оптимізатор портфеля послідовно повертає оптимізовані інвестиційні стратегії.

Посилання

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

Опитування щодо навчального посібника

Будь ласка, приділіть хвилину, щоб надати відгук про цей навчальний посібник. Ваші думки допоможуть нам покращити наші матеріали та досвід користувачів. Link to survey