1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「100兆通りの投資判断」を1秒で解く ― 量子ポートフォリオ最適化(QUBO + QAOA)をPythonで実装する

1
Posted at

はじめに

100銘柄から30銘柄を選ぶ組合せだけで 2京9000兆通り。さらに比率・リバランス・税金・流動性まで考慮すれば、現代のスーパーコンピュータでも厳密解には到達できません。資産運用業界はこの30年、「近似解で妥協する」 ことを前提にしてきました。

その妥協を終わらせようとしているのが、量子ポートフォリオ最適化 です。組合せ最適化は量子コンピュータがもっとも得意な領域のひとつ。量子アニーリング や QAOA(Quantum Approximate Optimization Algorithm) に落とし込めば、市場の変化と同じ秒スピードでポートフォリオを再計算できる世界が見えてきます。

「でも量子最適化ってGoldmanとかMUFGの世界でしょ?」――いえ、OpenJij / Qiskit / yfinance などのOSSを使えば、エンジニアが手元のPCで「実データ → QUBO定式化 → 量子(風)ソルバー → 効率的フロンティア比較」までを一気通貫で体験できます。

本記事では、note記事のテーマを開発者目線で落とし込み、Pythonで量子ポートフォリオ最適化の最小ひな形を構築する具体的な手順 を解説します。

参考にした解説記事:「100兆通りの投資判断」を1秒で解く——量子ポートフォリオ最適化が、資産運用業界の前提を破壊する日 | AI Robotics Quantum Lab

⚠️ 本記事は技術解説であり、特定の投資商品・取引を推奨するものではありません。投資判断はご自身の責任でお願いします。

ゴールは以下のとおりです。

  • yfinance で実データから 期待リターンと共分散行列 を作る
  • Markowitz型の最適化を QUBO に定式化する
  • OpenJij(D-Wave風 simulated annealing)で解く
  • Qiskit のQAOA でも同じ問題を解いてみる
  • 古典 SLSQP / cvxpy 解との比較で 「効率的フロンティア」 を描く

対象読者は、Pythonとnumpy/scikit-learn・基礎的な金融工学に触れたことがあるエンジニア・データサイエンティストです。


1. 全体像 ― 何を作るのか

[yfinance: 株価データ]
        │
        ▼
[期待リターン μ・共分散行列 Σ の推定]
        │
        ├──► [古典: cvxpy / scipy で連続解]   ──► 効率的フロンティア
        │
        └──► [QUBO化 (バイナリ選択+離散比率)]
                ├──► OpenJij SA       ──► 量子インスパイアード解
                └──► Qiskit QAOA      ──► 量子近似解(シミュレータ)
                                                │
                                                ▼
                                       [古典解との性能比較]

「量子マシンが古典より速く・正確に解ける問題のクラスを、自分で触って確かめる」 のがゴールです。


2. 環境構築

2.1 Pythonと仮想環境

mkdir quantum-portfolio && cd quantum-portfolio
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip

2.2 必要ライブラリ

# データ
pip install yfinance pandas numpy

# 古典最適化
pip install cvxpy scipy

# 量子(インスパイアード)
pip install openjij dimod              # SA / QUBO
pip install qiskit qiskit-aer          # 量子回路シミュレータ
pip install qiskit-optimization        # ポートフォリオ系の便利API
pip install qiskit-algorithms

# 可視化
pip install matplotlib seaborn

2.3 プロジェクト構成

quantum-portfolio/
├── data/
├── src/
│   ├── data.py        # 株価取得&統計量
│   ├── classical.py   # cvxpyベース
│   ├── qubo.py        # QUBO定式化
│   ├── openjij_solve.py
│   └── qaoa_solve.py
└── outputs/

3. データの取得と統計量推定

3.1 株価ヒストリカルを取る

# src/data.py
import yfinance as yf
import numpy as np
import pandas as pd


TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN", "META",
           "TSLA", "NVDA", "JPM", "WMT", "XOM"]


def load_returns(tickers: list[str] = TICKERS,
                 start: str = "2022-01-01",
                 end: str = "2024-12-31") -> pd.DataFrame:
    px = yf.download(tickers, start=start, end=end, auto_adjust=True)["Close"]
    rets = px.pct_change().dropna()
    return rets


def estimate_mu_sigma(rets: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
    mu = rets.mean().values * 252           # 年率期待リターン
    sigma = rets.cov().values * 252         # 年率共分散
    return mu, sigma

10銘柄×3年なら数秒で取得できます。実データで触ると一気に「投資の話」になります。


4. 古典:Markowitz の連続最適化(ベースライン)

cvxpy で 「期待リターン×λ − 0.5×分散」 を最大化する標準形を解きます。

# src/classical.py
import numpy as np
import cvxpy as cp


def solve_markowitz(mu: np.ndarray, sigma: np.ndarray,
                    risk_aversion: float = 5.0,
                    long_only: bool = True) -> np.ndarray:
    n = len(mu)
    w = cp.Variable(n)
    objective = cp.Maximize(mu @ w - 0.5 * risk_aversion * cp.quad_form(w, sigma))
    constraints = [cp.sum(w) == 1]
    if long_only:
        constraints.append(w >= 0)
    cp.Problem(objective, constraints).solve()
    return np.array(w.value).flatten()

これが 真の連続最適解。後段の量子解は、このベースラインと比較する形で評価します。


5. 問題をQUBOに落とし込む

量子アニーラ/QAOAが扱えるのは 0/1のバイナリ最適化(QUBO) です。連続な比率 w を、各銘柄について 「選ぶ/選ばない」 + 「どの離散比率を使うか」 のバイナリ表現に変換します。

5.1 離散ウェイトのバイナリ展開

各銘柄 i に対し、L 段階のウェイト候補(例: 0%, 10%, 20%, 30%)を割り当て、その中から One-Hot で1段階を選ぶ バイナリ変数 x_{i,l} を導入します。

# src/qubo.py
from itertools import product
import numpy as np
from dimod import BinaryQuadraticModel


def build_portfolio_qubo(
    mu: np.ndarray,
    sigma: np.ndarray,
    weight_levels: list[float] = (0.0, 0.1, 0.2, 0.3, 0.4),
    risk_aversion: float = 5.0,
    budget_penalty: float = 50.0,
    onehot_penalty: float = 50.0,
) -> BinaryQuadraticModel:
    n = len(mu)
    L = len(weight_levels)
    w_lv = np.array(weight_levels)

    # 変数: x_{i,l}  i: asset, l: level
    def var(i: int, l: int) -> str:
        return f"x_{i}_{l}"

    bqm = BinaryQuadraticModel("BINARY")
    for i in range(n):
        for l in range(L):
            bqm.add_variable(var(i, l), 0.0)

    # 目的: -mu^T w + 0.5 * lam * w^T Σ w
    # w_i = Σ_l weight_levels[l] * x_{i,l}
    # 1次項
    for i in range(n):
        for l in range(L):
            bqm.add_variable(var(i, l), -mu[i] * w_lv[l])

    # 2次項(共分散)
    for i in range(n):
        for j in range(n):
            for li in range(L):
                for lj in range(L):
                    coeff = 0.5 * risk_aversion * sigma[i, j] * w_lv[li] * w_lv[lj]
                    if i == j and li == lj:
                        bqm.add_linear(var(i, li), coeff)
                    else:
                        bqm.add_quadratic(var(i, li), var(j, lj), coeff)

    # ペナルティ1: One-Hot制約 (各銘柄1つだけ選ぶ)
    # ( Σ_l x_{i,l} - 1 )^2 を展開
    for i in range(n):
        for la in range(L):
            bqm.add_linear(var(i, la), -onehot_penalty)
            for lb in range(la + 1, L):
                bqm.add_quadratic(var(i, la), var(i, lb), 2 * onehot_penalty)
            bqm.add_linear(var(i, la), onehot_penalty)  # x^2 = x
        bqm.offset += onehot_penalty

    # ペナルティ2: 予算制約 ( Σ_i Σ_l weight_levels[l] * x_{i,l} - 1 )^2
    # 主要な係数だけ展開
    for i, j in product(range(n), repeat=2):
        for li, lj in product(range(L), repeat=2):
            coeff = budget_penalty * w_lv[li] * w_lv[lj]
            if i == j and li == lj:
                bqm.add_linear(var(i, li), coeff)
            else:
                bqm.add_quadratic(var(i, li), var(j, lj), coeff)
    for i in range(n):
        for l in range(L):
            bqm.add_linear(var(i, l), -2 * budget_penalty * w_lv[l])
    bqm.offset += budget_penalty
    return bqm

💡 上の実装はチュートリアル向けに分かりやすさ優先です。実務では qiskit_optimization の PortfolioOptimization や、pyqubo で式ベースに書く方が短くデバッグしやすくなります。


6. OpenJijで解く(量子インスパイアード)

D-Waveの量子アニーリングと同じQUBOを、OpenJij の Simulated Annealing で高速に解きます。

# src/openjij_solve.py
import numpy as np
import openjij as oj
from dimod import BinaryQuadraticModel

from src.qubo import build_portfolio_qubo


def solve_with_openjij(mu: np.ndarray, sigma: np.ndarray,
                       weight_levels=(0.0, 0.1, 0.2, 0.3, 0.4),
                       num_reads: int = 200) -> np.ndarray:
    bqm: BinaryQuadraticModel = build_portfolio_qubo(mu, sigma, weight_levels)
    sampler = oj.SASampler()
    qubo, offset = bqm.to_qubo()
    response = sampler.sample_qubo(qubo, num_reads=num_reads)

    best = response.first.sample
    n = len(mu)
    L = len(weight_levels)
    w = np.zeros(n)
    for i in range(n):
        for l in range(L):
            if best.get(f"x_{i}_{l}", 0) == 1:
                w[i] = weight_levels[l]
    if w.sum() > 0:
        w = w / w.sum()
    return w

num_reads を増やすほど、目的関数値の良い解に収束しやすくなります。量子アニーラの代替として、まずSAで動かす のが研究・PoCの定石です。


7. QiskitのQAOAで解く(量子回路ベース)

QAOAは NISQ実機 でも動かせる代表的な量子最適化手法です。QUBO ↔ Ising問題は等価変換できるので、Qiskit Optimizationの便利APIに渡せます。

# src/qaoa_solve.py
from qiskit_optimization import QuadraticProgram
from qiskit_optimization.converters import QuadraticProgramToQubo
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from qiskit_algorithms import QAOA
from qiskit_algorithms.optimizers import COBYLA
from qiskit.primitives import Sampler


def solve_with_qaoa(qp: QuadraticProgram, reps: int = 2):
    qubo = QuadraticProgramToQubo().convert(qp)
    qaoa = QAOA(sampler=Sampler(),
                optimizer=COBYLA(maxiter=200),
                reps=reps)
    optimizer = MinimumEigenOptimizer(qaoa)
    return optimizer.solve(qubo)

QuadraticProgram には Qiskit Optimization の PortfolioOptimization クラス で直接組み立てるのが最短です:

from qiskit_optimization.applications import PortfolioOptimization

po = PortfolioOptimization(
    expected_returns=mu,
    covariances=sigma,
    risk_factor=5.0,
    budget=3,                # 「3銘柄選ぶ」を明示する場合
)
qp = po.to_quadratic_program()
result = solve_with_qaoa(qp)
print(result.prettyprint())
  • 銘柄数 N が多すぎると、現在の量子シミュレータでは厳しいです。まずは N=6〜10 から試すのが現実的。
  • 実機(IBM Quantum)に投げたければ、Estimator/Sampler をRuntime版に差し替えます(前のV2G記事と同様)。

8. 古典 vs 量子(風)の比較

# 比較スクリプト例
from src.data import TICKERS, load_returns, estimate_mu_sigma
from src.classical import solve_markowitz
from src.openjij_solve import solve_with_openjij
import numpy as np


def evaluate(mu, sigma, w, label: str):
    ret = float(mu @ w)
    risk = float(np.sqrt(w @ sigma @ w))
    sharpe = ret / risk if risk > 0 else float("nan")
    print(f"[{label}] return={ret:.3f} risk={risk:.3f} sharpe={sharpe:.3f}")
    return ret, risk


rets = load_returns(TICKERS)
mu, sigma = estimate_mu_sigma(rets)

w_classical = solve_markowitz(mu, sigma, risk_aversion=5)
w_quantum   = solve_with_openjij(mu, sigma)

evaluate(mu, sigma, w_classical, "classical-cvxpy")
evaluate(mu, sigma, w_quantum,   "openjij-SA")

連続解(古典)は理論的最適、量子(風)は離散選択 + 制約ペナルティの結果になります。離散制約(最大銘柄数、ロット単位など)が増えるほど、古典が苦しくなり、QUBO系の優位が見えてきます。


9. 効率的フロンティアを描く

import matplotlib.pyplot as plt

risks, returns = [], []
for lam in np.linspace(0.5, 30, 25):
    w = solve_markowitz(mu, sigma, risk_aversion=lam)
    returns.append(float(mu @ w))
    risks.append(float(np.sqrt(w @ sigma @ w)))

plt.plot(risks, returns, "-o", label="Classical frontier")

# 量子(風)の点を重ねる
w_q = solve_with_openjij(mu, sigma)
plt.scatter([np.sqrt(w_q @ sigma @ w_q)], [mu @ w_q],
            color="red", s=80, label="OpenJij (QUBO)")
plt.xlabel("Risk (σ)")
plt.ylabel("Return (μ)")
plt.legend()
plt.savefig("outputs/frontier.png", dpi=150)

「赤点(QUBO解)が古典フロンティアの近くに乗っているか?」を確認するのが学習の入口です。


10. 実運用へ向けた次の一手

  • 離散制約をリアルにする:ロット単位(最低取引株数)、銘柄数上限、業種ウェイト上限など、現実の運用が抱える制約こそ量子最適化が映える領域。
  • 動的最適化:1日/1時間ごとに再ソルブし、リバランスコスト(取引手数料)も目的関数に乗せる。
  • オルタナティブデータ:衛星画像、SNSセンチメント、サプライチェーンデータをμの推定に取り込む。
  • ロバスト最適化:μとΣが不確実な世界で、ワーストケース性能を最大化(Robust Portfolio Optimization)。
  • D-Wave 実機:dwave-ocean-sdk のLeapを使えば、量子アニーラ実機にジョブを投げられる(無料枠あり)。
  • XAI(説明可能AI):「なぜこの銘柄が選ばれたか」を、寄与度分解(Shapley値の量子版含む)で説明できるダッシュボードを作る。

11. 法令・運用上の注意

  • 日本で投資助言・運用業を行うには 金融商品取引法上の登録(投資助言・代理業/投資運用業)が必要です。社内検証・自己資産運用の範囲を超える場合は要注意。
  • 量子最適化結果も ヒューマンレビュー を残すのが現実的。「アルゴリズムの説明責任(XAI)」を最初から設計に組み込む。
  • 過学習リスク:μとΣは過去の標本から推定したもので、量子で精緻化しても 将来予測の保証はない。
  • バックテスト時の ルックアヘッドバイアス・サバイバーシップバイアス に注意。
  • 取引コスト・スリッページ・税金まで含めないと、紙上の最適化が実運用で意味を持たない。

12. 拡張アイデア

  • 量子+古典のハイブリッドソルバー(QBSolv 系)でN=数百銘柄級に挑戦
  • 強化学習×量子:政策ネットワーク内で量子最適化を呼ぶ
  • 個人向け 量子ロボアド:少額ETF複数本を、ロット単位制約で個別最適化
  • テンソルネットワーク で大規模Σを低ランク近似してから量子に乗せる
  • Continuous Relaxation + Rounding ベンチマーク:量子がいつ古典を上回るかの実験設計

まとめ

  • ポートフォリオ最適化は組合せ問題に落ちやすく、QUBO + 量子アニーリング / QAOA とは相性が良い
  • 本記事のひな形で、実データ取得 → 古典cvxpy → QUBO定式化 → OpenJij SA → Qiskit QAOA までを最短ルートで体験できる
  • N=10程度の小規模問題でも、「離散制約が増えると量子(風)アプローチの存在価値が見えてくる」感覚は十分つかめる
  • 実運用では金商法・XAI・データ品質などの壁が増えるが、設計の初手はノートPCで回せる
  • 「100兆通りを1秒で解く」未来の最初の歯車は、自分のノートPCで回せる

まずは10銘柄でMarkowitz古典解を出し、同じ問題をOpenJijで解いて、両者の差を観察してみてください。「量子(風)最適化が金融でなぜ熱いのか」 が、コードの手触りから分かります。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?