Skip to main content
IBM Quantum Platform

Global Data Quantumのポートフォリオ・オプティマイザーによる動的なポートフォリオ最適化

Qiskit 関数は、 IBM Quantum® Premium Plan、Flex Plan、およびオンプレム ( IBM Quantum Platform API 経由) Plan ユーザーだけが利用できる実験的な機能です。 これらはプレビューリリースであり、変更される可能性がある。

使用時間の目安:Heron r2 プロセッサで約55分。 (注:これはあくまでも目安です。 実際の走行時間は異なるかもしれない)。


背景

動的ポートフォリオ最適化問題は、複数の期間にわたって、期待されるポートフォリオのリターンを最大化し、リスクを最小化する最適な投資戦略を見つけることを目的としており、多くの場合、予算、取引コスト、リスク回避などの特定の制約の下で行われる。 ポートフォリオのリバランスを1回で行う標準的なポートフォリオ最適化とは異なり、ダイナミック・バージョンは資産の進化を考慮し、時間の経過に伴う資産パフォーマンスの変化に基づいて投資を適応させる。

このチュートリアルでは、Quantum Portfolio Optimizer Qiskit Functionを使用して動的ポートフォリオ最適化を実行する方法を示します。 具体的には、この応用関数を使って複数の時間ステップにわたる投資配分問題を解く方法を説明する。

このアプローチでは、ポートフォリオ最適化を多目的二元制約最適化(QUBO)問題として定式化する。 具体的には、4つの異なる目的を同時に最適化するためにQUBO関数 OO

  • 戻り関数を最大化する FF
  • 投資のリスクを最小限に抑える RR
  • 取引コストの最小化 CC
  • PP を最小化するために追加的に設定された投資制限を遵守すること。

まとめると、これらの目的に取り組むために、QUBO関数を次のように定式化する。 O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, ここで、 γ\gamma はリスク回避係数、 ρ\rho は制限強化係数(ラグランジュ乗数)である。 明示的な定式化は式(1)にある。 [1] の(15)である。

Variational Quantum Eigensolver (VQE)に基づくハイブリッド量子古典法を用いて解く。 このセットアップでは、量子回路がコスト関数を推定し、古典的最適化は差分進化アルゴリズムを用いて実行される。 必要な量子ビットの数は、3つの主な要因に依存する:資産の数 na、時間期間の数 nt、投資を表現するために使用されるビット分解能 nq。 具体的には、この問題の最小量子ビット数は na*nt*nq である。

このチュートリアルでは、スペインのIBEX35指数に基づく地域別ポートフォリオの最適化に焦点を当てる。 具体的には、下表に示す7つの資産ポートフォリオを使用している:

IBEX35ポートフォリオ
ACS.MC
ITX.MC
FER.MC
ELE.MC
SCYR.MC
AENA.MC
AMS.MC

ポートフォリオのリバランスは、2022年11月1日から30日間隔で4回実施する。 各投資変数は2ビットで符号化される。 この結果、問題を解くのに56量子ビットを必要とする。

私たちは、この種の金融最適化問題のパフォーマンスを向上させるために特別に調整された、標準的なReal Amplitudes assatzをカスタマイズし、ハードウェア効率よく適応させたOptimized Real Amplitudes assatzを使用しています。

クォンタムの実行は、 ibm_torino バックエンドで行われる。 問題の定式化、方法論、性能評価に関する詳細な説明は、公表された原稿 [1] を参照されたい。


要件

!pip install qiskit-ibm-catalog
!pip install pandas
!pip install matplotlib
!pip install yfinance

セットアップ

Quantum Portfolio Optimizerを使用するには、Qiskit Functions Catalogから機能を選択します。 この機能を実行するには、 IBM Quantum Premium PlanまたはFlex PlanアカウントとGlobal Data Quantumからのライセンスが必要です。

まず、 APIキー で認証を行ってください。 次に、 Qiskit Functions Catalog から目的の関数を読み込みます。 ここでは、クラス QiskitFunctionsCatalog を使用して、カタログ内の関数 quantum_portfolio_optimizer にアクセスしています。 この関数を使用すると、Quantumの Portfolio Optimization ソルバーを利用できます。

from qiskit_ibm_catalog import QiskitFunctionsCatalog

catalog = QiskitFunctionsCatalog(
    channel="ibm_quantum_platform",
    instance="INSTANCE_CRN",
    # For `token`, use the 44-character API_KEY you created
    # and saved from the IBM Quantum Platform Home dashboard
    token="YOUR_API_KEY",
)

# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")

ステップ1: 入力ポートフォリオを読み込む

このステップでは、IBEX35指数から選択した7つの資産のヒストリカルデータを、特に 2022年11月1 日から 2023年4月1 日までロードする。

ヤフーファイナンスの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
assets = pd.concat(series_list, axis=1)

# Convert index to string for consistency
assets.index = assets.index.astype(str)

Output:

[*********************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...
警告

Yahoo Finance APIからデータが正しくダウンロードされたことを確認するため、以下のセルを実行することをお勧めします。 ダウンロードが失敗した場合、通知が表示され、代わりに代替オンラインデータベースからデータを取得することが示されます。

if (
    assets.isna().values.any()
):  # If has nan values, load the data from the fallback online database
    print(
        "WARNING: Asset data format is invalid or incomplete. "
        "Loading data from the remote backup database."
    )

    url = "https://raw.githubusercontent.com/Global-Data-Quantum/PortfolioData/1f3ebb95fdef245014a04d4273f688a2951e0061/data/qpo_tutorial_ibex.csv"
    assets = pd.read_csv(url, index_col=0)

ステップ2: 問題の入力定義

QUBO問題を定義するために必要なパラメータは、 qubo_settings 辞書で設定される。 時間ステップ数(nt)、投資指定のビット数(nq)、各時間ステップの時間窓(dt)を定義する。さらに、資産ごとの最大投資額、リスク回避係数、取引手数料、制限係数を設定する(問題定式化の詳細については我々の論文を参照)。 これらの設定により、QUBO問題を特定の投資シナリオに適合させることができる。

qubo_settings = {
    "nt": 4,
    "nq": 2,
    "dt": 30,
    # maximum investment per asset is 2**nq/max_investment = 80%
    "max_investment": 5,
    "risk_aversion": 1000.0,
    "transaction_fee": 0.01,
    "restriction_coeff": 1.0,
}

この optimizer_settings 辞書は、反復回数や1世代 population_size あたりの候補解 num_generations の数などのパラメータを含め、最適化プロセスを設定します。 その他の設定では、組換え率、並列ジョブ数、バッチサイズ、変異範囲などの項目を制御します。 さらに、、 estimator_precisionestimator_shotsなどのプリミティブ設定は、最適 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,
    },
}
Note

回路総数は optimizer_settings のパラメータに依存し、 (num_generations + 1) * population_size として計算される。

辞書 ansatz_settings は量子回路アンザッツを設定する。 この ansatz パラメータは、金融最適化問題向けに設計されたハードウェア効率の高い "optimized_real_amplitudes" アプローチの使用を指定します。 複数のパスマネージャー(デフォルトのローカルQiskitパスマネージャーやQiskit AI搭載トランスパイラサービスを含む)を使用して回路最適化の性能と効率を向上させるオプション multiple_passmanager は、現在falseに設定されています。この機能は一時的に無効化されているためです。

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

最後に、準備された入力を渡して dpo_solver.run() 関数を実行し、最適化を実行する。 これには、アセットデータディクショナリ(assets)、QUBO設定(qubo_settings)、最適化パラメータ(optimizer_settings)、量子回路ansatz設定(ansatz_settings)が含まれます。さらに、バックエンドや結果に後処理を適用するかどうかなど、実行の詳細を指定します。 これにより、選択された量子バックエンド上で動的ポートフォリオ最適化プロセスが開始される。

dpo_job = dpo_solver.run(
    assets=assets.to_dict(),  # Insert assets as a dictionary
    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"]

Output:

{'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}")

Output:

Minimum Objective Cost Found: -3.67
Best Solution:
  - Restriction Deviation: 40.0%
  - Sharpe Ratio: 14.54
  - Return: 0.28

以下のコードは、最適化アルゴリズムのコスト分布をランダムサンプリング分布と可視化して比較する方法を示しています。 同様に、ランダムな投資で評価することで、QUBO目的関数(関数出力から読み込める)のランドスケープを探索する。 両方の分布を振幅で正規化してプロットし、最適化プロセスがランダムサンプリングと比べてコスト面でどのように異なるかを比較しやすくした。 さらに、DOCPlexを用いて得られた結果は、従来のベンチマークとして機能する破線の垂直基準線として含まれている。 我々は、同じ問題を従来の手法で解くために、数学的最適化のための IBM® オープンソースライブラリであるPython DOCPlexの無料版を使用する。

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"][
    "counts"
]  # List of how many times each solution occurred
cost_list = dpo_result["metadata"]["all_samples_metrics"][
    "objective_costs"
]  # 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:

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 ほか. 「動的 Portfolio Optimization のための変分量子固有値ソルバーのスケーリング」 arXiv プレプリント arXiv:2412.19150 (2024).


チュートリアル調査

このチュートリアルに関するフィードバックをお寄せください。 あなたの洞察は、私たちのコンテンツの提供とユーザーエクスペリエンスを向上させるのに役立ちます。 アンケートへのリンク

このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。