はじめに
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で解いて、両者の差を観察してみてください。「量子(風)最適化が金融でなぜ熱いのか」 が、コードの手触りから分かります。