דלג לתוכן הראשי

ביצוע אופטימיזציה דינמית של תיק השקעות עם Portfolio Optimizer של Global Data Quantum

הערה

פונקציות Qiskit הן תכונה ניסיונית הזמינה רק למשתמשי IBM Quantum® Premium Plan, Flex Plan ו-On-Prem (דרך IBM Quantum Platform API) Plan. הן במצב שחרור מקדים וכפופות לשינויים.

הערכת שימוש: כ-55 דקות על מעבד Heron r2. (הערה: זוהי הערכה בלבד. זמן הריצה בפועל עשוי להשתנות.)

רקע

בעיית אופטימיזציית תיק השקעות דינמית מכוונת למציאת אסטרטגיית השקעה אופטימלית על פני תקופות זמן מרובות כדי למקסם את התשואה הצפויה של התיק ולמזער סיכונים, לעיתים קרובות תחת אילוצים מסוימים כגון תקציב, עלויות עסקה או סלידה מסיכון. בניגוד לאופטימיזציית תיק השקעות סטנדרטית, המתייחסת לזמן בודד לאיזון מחדש של התיק, הגרסה הדינמית לוקחת בחשבון את האופי המתפתח של נכסים ומתאימה השקעות על סמך שינויים בביצועי הנכסים לאורך זמן.

מדריך זה מדגים כיצד לבצע אופטימיזציה דינמית של תיק השקעות באמצעות פונקציית Qiskit Quantum Portfolio Optimizer. במיוחד, אנו ממחישים כיצד להשתמש בפונקציית יישום זו כדי לפתור בעיית הקצאת השקעות על פני צעדי זמן מרובים.

הגישה כוללת ניסוח אופטימיזציית התיק כבעיית Quadratic Unconstrained Binary Optimization (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].

אנו פותרים באמצעות שיטה קוונטית-קלאסית היברידית המבוססת על Variational Quantum Eigensolver (VQE). במערך זה, המעגל הקוונטי מעריך את פונקציית העלות, בעוד שהאופטימיזציה הקלאסית מבוצעת באמצעות אלגוריתם Differential Evolution, המאפשר ניווט יעיל בנוף הפתרון. מספר הקיוביטים הנדרש תלוי בשלושה גורמים עיקריים: מספר הנכסים na, מספר תקופות הזמן nt ורזולוציית הביטים המשמשת לייצוג ההשקעה nq. במיוחד, מספר הקיוביטים המינימלי בבעיה שלנו הוא na*nt*nq.

למדריך זה, אנו מתמקדים באופטימיזציה של תיק אזורי המבוסס על מדד IBEX 35 הספרדי. במיוחד, אנו משתמשים בתיק של שבעה נכסים כפי שמצוין בטבלה להלן:

תיק IBEX 35ACS.MCITX.MCFER.MCELE.MCSCYR.MCAENA.MCAMS.MC

אנו מאזנים מחדש את התיק שלנו בארבעה צעדי זמן, כל אחד מופרד במרווח של 30 יום החל מ-1 בנובמבר 2022. כל משתנה השקעה מקודד באמצעות שני ביטים. זה מביא לבעיה הדורשת 56 קיוביטים לפתרון.

אנו משתמשים ב-Optimized Real Amplitudes ansatz, התאמה מותאמת אישית ויעילה מבחינת חומרה של ה-Real Amplitudes ansatz הסטנדרטי, המותאמת במיוחד לשיפור הביצועים עבור סוג זה של בעיית אופטימיזציה פיננסית.

הביצוע הקוונטי מבוצע על ה-backend 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

הגדרה

כדי להשתמש ב-Quantum Portfolio Optimizer, בחר את הפונקציה דרך קטלוג פונקציות Qiskit. אתה זקוק לחשבון IBM Quantum Premium Plan או Flex Plan עם רישיון מ-Global Data Quantum כדי להפעיל פונקציה זו.

תחילה, אמת עם מפתח ה-API שלך. לאחר מכן, טען את הפונקציה הרצויה מקטלוג פונקציות Qiskit. כאן, אתה ניגש לפונקציה quantum_portfolio_optimizer מהקטלוג באמצעות המחלקה QiskitFunctionsCatalog. פונקציה זו מאפשרת לנו להשתמש בפותר 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")

שלב 1: קריאת תיק הקלט

בשלב זה, אנו טוענים נתונים היסטוריים עבור שבעת הנכסים הנבחרים ממדד IBEX 35, במיוחד מ-1 בנובמבר 2022 עד 1 באפריל 2023.

אנו מביאים את הנתונים באמצעות Yahoo Finance API, תוך התמקדות במחירי סגירה. הנתונים עוברים עיבוד כדי להבטיח שלכל הנכסים יש את אותו מספר ימים עם נתונים. כל נתון חסר (ימים שאינם ימי מסחר) מטופל בהתאם, תוך הבטחה שכל הנכסים מיושרים על אותם תאריכים.

הנתונים מובנים ב-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, מגדירות את תצורת ה-estimator וה-sampler הקוונטיות עבור תהליך האופטימיזציה.

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 של המעגל הקוונטי. הפרמטר ansatz מציין את השימוש בגישת "optimized_real_amplitudes", שהוא ansatz יעיל מבחינת חומרה המיועד לבעיות אופטימיזציה פיננסיות. בנוסף, הגדרת multiple_passmanager מאופשרת כדי לאפשר מספר מנהלי מעבר (כולל מנהל המעבר המקומי הסטנדרטי של Qiskit ושירות ה-transpiler המונע בינה מלאכותית של Qiskit) במהלך תהליך האופטימיזציה, תוך שיפור הביצועים הכוללים והיעילות של ביצוע המעגל.

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

לבסוף, אנו מבצעים את האופטימיזציה על ידי הרצת הפונקציה dpo_solver.run(), תוך העברת הקלטים המוכנים. אלה כוללים את מילון נתוני הנכסים (assets), את תצורת QUBO (qubo_settings), פרמטרי אופטימיזציה (optimizer_settings) ואת הגדרות ה-ansatz של המעגל הקוונטי (ansatz_settings). בנוסף, אנו מציינים את פרטי הביצוע כגון ה-backend, והאם להחיל עיבוד שלאחר על התוצאות. זה מתחיל את תהליך אופטימיזציית תיק ההשקעות הדינמית על ה-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,
)

שלב 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).

סקר מדריך

אנא הקדש דקה למתן משוב על מדריך זה. התובנות שלך יעזרו לנו לשפר את הצעות התוכן ואת חוויית המשתמש שלנו. קישור לסקר