動画:ポートフォリオ最適化(Python)
動画「QUBO++ ケーススタディ ポートフォリオ最適化(Python)」の手引きです。 章ごとに、要点と動画に出てくるプログラム・出力、詳しい説明のページへのリンクをまとめています。 「▶ 動画の … から再生」をクリックすると、その場面から再生します。
この動画で扱うこと:
- 定期預金と株の比較から、期待リターン・リスク(標準偏差)・リスクプレミアムの考え方
- 相関係数と、株を組み合わせるとリスクが下がる理由(相関がなければ下がり、相関係数が 1 なら下がらない)
- 期待リターン $R$ とリスクの 2 乗 $V$(分散)の式と、$V - \lambda R$ を最小にする平均分散モデル
- 100 株単位の単元数を整数変数で、予算の範囲を
qbpp.cons()で書き、実数係数のモジュールpyqbpp.dで解くこと - 買う銘柄の数を 4 つまでに制限すること
文章での説明はポートフォリオ最適化のページにあります。動画(ケーススタディ)の一覧に戻る
この動画の銘柄とデータは架空のもので、投資の判断に使うものではありません。
預金と株
- 1000 万円を 1 年間置いておくとします。定期預金なら、金利が例えば 2% のとき、1 年後にほぼ確実に 1020 万円になります。
- 株 X は、値上がりと配当で、平均して 1 年に 10% 増えると見込まれています。平均の増え方の見込みを期待値、年ごとのぶれの大きさを標準偏差と呼びます。株 X の標準偏差は 30% とします。
- 増え方が山の形(正規分布)に分布するとすると、およそ 3 分の 2 の年は −20%〜+40%、95% の年は −50%〜+70% に入ります。損をする年はおよそ 37%、10 年に 4 年近くあります。
- 宝くじや賭け事と違い、期待値はプラスです。期待値の 10% と預金の 2% の差 8% をリスクプレミアムと呼びます。ぶれを引き受ける見返りとして、預金より高い期待値が付きます。
- 以下では、標準偏差をリスク、期待値を期待リターンと呼びます。
相関のない2つの株
- 株 Y も、期待リターン 10%、リスク 30% とします。2 つの株の動き方の関係は相関係数 $\rho$ で表します。いつもそろって動くなら 1、まったく関係なく動くなら 0、逆に動く傾向があるとマイナスです。
- $\rho = 0$ の株 X と Y を 500 万円ずつ買うと、期待リターンは 10% のままで、リスクは 30% から 21.2% に下がります。片方が大きく下がっても、もう片方まで大きく下がるとは限らないからです。
- リスクの 2 乗を分散と呼びます。株 X、Y に使う金額の割合を $w_1, w_2$、リスクを $\sigma_1, \sigma_2$ とすると、$\rho = 0$ のとき、合計のリスクの 2 乗は 2 つのぶれ $w_1\sigma_1$、$w_2\sigma_2$ の 2 乗の和です。
- 半分ずつ買うので $w_1\sigma_1 = w_2\sigma_2 = 15\%$ で、$\sigma^2 = 15^2 + 15^2 = 450$、$\sigma = \sqrt{450} = 21.2\%$ です。
- 2 つのぶれを長さ 15 の 2 本の矢印で表すと、$\rho = 0$ では直角に交わり、合わせた矢印の長さが 21.2 になります。15 と 15 を足しても 30 にはなりません。
相関とリスク
- 実際の株は同じ市場で取引されるので、相関係数はふつうプラスです。$\rho$ が 0 でないとき、リスクの 2 乗に項が 1 つ加わり、2 つの株がそろって動く分だけ大きくなります。
- 半分ずつ買ったときのリスクは次のとおりです。矢印の図では、$\rho$ が大きいほど 2 本の矢印の間の角度が小さくなります。
| $\rho$ | 0 | 0.1 | 0.2 | 0.3 | 0.6 | 0.9 | 1 |
|---|---|---|---|---|---|---|---|
| リスク | 21.2% | 22.2% | 23.2% | 24.2% | 26.8% | 29.2% | 30% |
- $\rho = 1$ では 2 本の矢印が一直線に並び、リスクは 1 つの株だけを買ったときと同じ 30% です。相関係数が 1 なら分けて買う意味はなく、1 より小さいほど、分けて買うとリスクが下がります。
- 株の組み合わせをポートフォリオ、分けて買うことを分散投資と呼びます。
- どの 2 つも $\rho = 0.2$ の株を同じ金額ずつ買うと、リスクは 1 銘柄で 30%、2 銘柄で 23.2%、5 銘柄で 18.0%、10 銘柄で 15.9% と下がりますが、いくら増やしても 13.4% より下にはなりません。残るのは市場全体がそろって上がったり下がったりする分で、リスクプレミアムが付くのはこの分です。
平均分散モデル
- 銘柄 $i$ に使う金額の割合を $w_i$、銘柄 $i$ の期待リターンを $\mu_i$、リスクを $\sigma_i$、銘柄 $i$ と銘柄 $j$ の相関係数を $\rho_{ij}$($\rho_{ii} = 1$)とします。
- 期待リターン $R$ と、ポートフォリオ全体のリスクの 2 乗(分散)$V$ は次のとおりです。
- $V$ のうち $i = j$ の項は $(w_i\sigma_i)^2$ です。$i \ne j$ の組は $ij$ と $ji$ の 2 回現れ、$2\rho_{ij} \times w_i\sigma_i \times w_j\sigma_j$ の項になります。銘柄が 2 つなら、前の章の 2 つの株の式と同じです。リスクは $V$ の平方根なので、$V$ を小さくすればリスクも小さくなります。
- $V$ は小さいほど、$R$ は大きいほどうれしい量です。2 つを 1 つの式にまとめるため、$R$ に係数 $\lambda$ を掛けて $V$ から引いた $V - \lambda R$ を最小にします。$\lambda$ を大きくするほど $R$ を重視します。これがマーコウィッツの平均分散モデルです。
問題
- 架空の 10 銘柄(5 業種 × 2)を、予算 1000 万円で買います。
| 銘柄 | 業種 | 株価(円) | 期待リターン | リスク |
|---|---|---|---|---|
| A | 電機 | 2,450 | 11.0% | 32% |
| B | 電機 | 6,180 | 10.0% | 28% |
| C | 自動車 | 1,870 | 8.0% | 26% |
| D | 自動車 | 3,320 | 9.0% | 30% |
| E | 銀行 | 1,240 | 7.0% | 24% |
| F | 銀行 | 4,060 | 6.5% | 22% |
| G | 食品 | 2,890 | 4.5% | 15% |
| H | 食品 | 5,510 | 5.0% | 17% |
| I | 通信 | 1,530 | 4.0% | 14% |
| J | 通信 | 3,970 | 5.5% | 18% |
- 相関係数は、同じ業種なら 0.6、違う業種なら 0.2 とします。同じ業種の株は、そろって動きやすいからです。
- 株は 100 株を 1 単元として、単元単位でしか買えません。1 つの銘柄に使えるのは、予算の 30%、300 万円までとします。
単元と予算
- 銘柄 $i$ の単元数を $x_i$(0 以上の整数)、1 単元の金額を $c_i$、予算を $B$ とすると、金額の割合は $w_i = c_i x_i / B$ です。例えば銘柄 A は株価 2,450 円なので、1 単元は 24 万 5 千円です。
- 割合を単元数で書き換えると、$R$ は $x_i$ の 1 次式、$V$ は 2 次式になります。$V$ の式に現れる $\rho_{ij}\sigma_i\sigma_j$ は 10 × 10 の表に並び、同じ業種の組には相関係数 0.6 が掛かります。
- 予算の制約として、合計金額を 980 万円以上、1000 万円以下とします。下限がないと、$\lambda = 0$ のとき何も買わないのが最良になってしまいます。使わずに残ったお金は、リスクも期待リターンも 0 です。
- 単元数の上限 $u_i$ は、300 万円までで買える単元数です(銘柄 A なら 12 単元、294 万円)。単元数の組み合わせは、予算を考えなければおよそ 233 億通りです。
詳しくはポートフォリオ最適化のページをご覧ください。
QUBO++での書き方
- 実数の係数:期待リターンやリスクは小数なので、目的関数の係数も小数になります。
import pyqbpp.d as qbppで実数係数のモジュールを使います。QUBO++ が内部で係数を整数に直してから解きます。 - 整数変数:
qbpp.var("x", shape=n, integer=(0, cap))で単元数の整数変数の配列を作ります。上限capをリストで渡すと、銘柄ごとに違う上限になります。整数変数は 2 値の変数に分けずに、整数のまま探索されます。 - 範囲の制約:予算は
qbpp.cons(cost, between=(9800, budget))で書きます。制約を破ると、重み × 違反量² がペナルティとして加わります。不等式の制約でも補助の変数は要りません。 - 制約の重みは 1 で十分です。金額は千円単位の整数なので、範囲を外れると少なくとも 1 ずれ、ペナルティは 1 以上になります。一方、目的関数の値は 0.1 にも満たないので、予算の範囲を外れた解は、どれも範囲内の解より悪くなります。
詳しくは実数(double)係数・整数変数・制約のページをご覧ください。
プログラム
動画のプログラムです(文章のページのプログラムと同じで、5 行目のコメントだけ「リスク」にしています)。
import math
import pyqbpp.d as qbpp
# 銘柄 A〜J の業種、株価(円)、期待リターン、リスク(年率)
sector = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
price = [2450, 6180, 1870, 3320, 1240, 4060, 2890, 5510, 1530, 3970]
mu = [0.11, 0.10, 0.08, 0.09, 0.07, 0.065, 0.045, 0.05, 0.04, 0.055]
sigma = [0.32, 0.28, 0.26, 0.30, 0.24, 0.22, 0.15, 0.17, 0.14, 0.18]
n = len(price)
budget = 10000 # 千円
lam = 0.5
# 1 単元(100 株)の金額(千円)と、予算の 30% までで買える単元数
lot = [p // 10 for p in price]
cap = [3000 // c for c in lot]
x = qbpp.var("x", shape=n, integer=(0, cap))
risk = 0
ret = 0
cost = 0
for i in range(n):
wi = lot[i] / budget
for j in range(n):
wj = lot[j] / budget
rho = 1.0 if i == j else (0.6 if sector[i] == sector[j] else 0.2)
risk += rho * sigma[i] * sigma[j] * wi * wj * x[i] * x[j]
ret += mu[i] * wi * x[i]
cost += lot[i] * x[i]
f = risk - lam * ret + qbpp.cons(cost, between=(9800, budget))
f.simplify()
sol = qbpp.EasySolver(f).search(time_limit=1.0)
for i in range(n):
if sol(x[i]) > 0:
print(f"{chr(ord('A') + i)}: {sol(x[i]):2.0f} lots ({lot[i] * sol(x[i]):4.0f} thousand yen)")
print(f"cost = {sol(cost):.0f} thousand yen, return = {100 * sol(ret):.2f}%, "
f"risk = {100 * math.sqrt(sol(risk)):.2f}%")
lotは 1 単元(100 株)の金額で、千円単位にするため株価を 10 で割ります。capは 300 万円までで買える単元数です。risk、ret、costは、$V$、$R$、合計金額の式です。wi・wjは 1 単元が予算に占める割合、rhoは相関係数(同じ銘柄は 1、同じ業種は 0.6、違う業種は 0.2)で、二重のループで $V$ の 100 個の項を足し合わせます。fは $V - \lambda R$ に予算の制約を加えた式です。整数変数だけの式なので、simplify()で整理します。EasySolverで解を探し、time_limitで探索の時間を 1 秒にします。sol(x[i])は解での銘柄iの単元数です。リスクはriskの値の平方根で、期待リターンとリスクは 100 倍してパーセントで表示します。
実行結果
A: 4 lots ( 980 thousand yen)
B: 2 lots (1236 thousand yen)
C: 5 lots ( 935 thousand yen)
D: 2 lots ( 664 thousand yen)
E: 7 lots ( 868 thousand yen)
F: 2 lots ( 812 thousand yen)
G: 4 lots (1156 thousand yen)
H: 2 lots (1102 thousand yen)
I: 4 lots ( 612 thousand yen)
J: 4 lots (1588 thousand yen)
cost = 9953 thousand yen, return = 6.98%, risk = 12.75%
- 10 銘柄すべてを、少しずつ買っています。例えば銘柄 A は 4 単元、98 万円です。合計金額は 995 万 3 千円で、予算の範囲に入っています。
- 期待リターンは 6.98%、リスクは 12.75% です。リスク 12.75% は、どの銘柄のリスク(最も小さい I でも 14%)よりも小さく、分けて買う効果が表れています。
銘柄数の制限
- 買う銘柄を 4 つまでに制限します。銘柄 $i$ を買うとき 1 になる 2 値の変数 $y_i$ を加え、次の 2 つの制約を課します。
- $y_i = 0$ なら $x_i \le 0$ なので銘柄 $i$ は買えず、$y_i = 1$ なら $x_i \le u_i$ で、もとの範囲と同じです。
- プログラムでは、予算の制約の後に次の行を加えます。
x - qbpp.array(cap) * yは要素ごとに計算した 10 個の式の配列で、0 以下と比べてqbpp.cons()で包むと 10 本の制約になります。2 値変数を含むので、整理はsimplify_as_binary()で行います。
# 買う銘柄は 4 つまで: 銘柄 i を買うとき y[i] = 1
y = qbpp.var("y", n)
f += qbpp.cons(x - qbpp.array(cap) * y <= 0)
f += qbpp.cons(qbpp.sum(y) <= 4)
f.simplify_as_binary()
B: 4 lots (2472 thousand yen)
C: 9 lots (1683 thousand yen)
G: 10 lots (2890 thousand yen)
J: 7 lots (2779 thousand yen)
cost = 9824 thousand yen, return = 6.65%, risk = 13.19%
- 電機・自動車・食品・通信から 1 つずつ、業種が重ならないように選ばれています。銘柄を絞った分、期待リターンは 6.65% に下がり、リスクは 13.19% に上がりました。
まとめ
- 預金と株を比べ、期待リターン、リスク、リスクプレミアムを説明しました。相関係数が 1 より小さい株を組み合わせると、リスクが下がります。
- 期待リターンとリスクのバランスは、$V - \lambda R$ の最小化で決めます。
- QUBO++ では、実数の係数、整数変数、
qbpp.cons()でそのまま書けます。100 株単位、予算の範囲、銘柄の数の制限も、制約として加えられます。