ポートフォリオ最適化
予算 1000 万円で、10 銘柄の株を組み合わせて買います。 株は 100 株(1 単元)単位でしか買えず、1 銘柄に使えるのは予算の 30% までです。 期待リターン(1 年間に見込める値上がりと配当の割合)が大きく、リスク(リターンのばらつき)が小さい組合せを求めます。
銘柄は次のとおりです(架空のデータです)。 ボラティリティは 1 年間のリターンの標準偏差です。
| 銘柄 | 業種 | 株価(円) | 期待リターン | ボラティリティ |
|---|---|---|---|---|
| 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% |
2 つの銘柄のリターンの相関係数は、同じ業種なら 0.6、違う業種なら 0.2 とします。 同じ業種の銘柄はそろって値動きしやすいので、業種を分けて買うほどリスクが下がります。
定式化
銘柄 $i$ の単元数を整数変数 $x_i$ とします。 1 単元の金額を $c_i$(千円)、予算を $B = 10000$(千円)とすると、銘柄 $i$ が予算に占める割合は $w_i = c_i x_i / B$ です。 このとき、ポートフォリオの期待リターンとリスク(分散)は次のとおりです。
\[R = \sum_{i} \mu_i w_i, \qquad V = \sum_{i} \sum_{j} \rho_{ij}\, \sigma_i \sigma_j\, w_i w_j\]ここで $\mu_i$ は期待リターン、$\sigma_i$ はボラティリティ、$\rho_{ij}$ は相関係数($\rho_{ii} = 1$)です。 リスクを小さく、リターンを大きくしたいので、重み $\lambda$ を付けて次のように定式化します。
\[\begin{aligned} \text{最小化}\quad & V - \lambda R \\ \text{制約}\quad & 9800 \le \sum_{i} c_i x_i \le 10000 \\ & 0 \le x_i \le u_i && \text{(すべての銘柄 } i\text{)} \end{aligned}\]$u_i$ は予算の 30%(3000 千円)までで買える単元数です。 予算は 980 万円以上使うことにします(残りは現金で、リスクもリターンも 0 です)。 $\lambda$ はリターンをどれだけ重視するかを表し、$\lambda = 0$ ならリスク最小、$\lambda$ が大きいほどリターン重視になります。 これは平均分散モデル(Markowitz モデル)で、 割合 $w_i$ を自由に選べるなら凸二次計画問題として効率よく解けますが、 単元の制約や、次の節の銘柄数の制限が入ると組合せ最適化問題になります。
係数は実数なので、実数(double)係数(#define DOUBLE_TYPE)を使います。 単元数 $x_i$ はネイティブ整数変数、予算の制約は qbpp::cons() で書きます。 金額は千円単位の整数なので、予算の制約を破ると金額は少なくとも 1 ずれ、ペナルティは 1 以上になります。 目的関数の値はこの問題では 0.1 に満たないので、制約の重みは 1 で十分です。
プログラム
$\lambda = 0.5$ として解きます。
#define DOUBLE_TYPE
#include <qbpp/qbpp.hpp>
#include <qbpp/easy_solver.hpp>
#include <cmath>
#include <cstdio>
#include <vector>
int main() {
// 銘柄 A〜J の業種、株価(円)、期待リターン、ボラティリティ(年率)
std::vector<int> sector = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4};
std::vector<int> price = {2450, 6180, 1870, 3320, 1240, 4060, 2890, 5510, 1530, 3970};
std::vector<double> mu = {0.11, 0.10, 0.08, 0.09, 0.07, 0.065, 0.045, 0.05, 0.04, 0.055};
std::vector<double> sigma = {0.32, 0.28, 0.26, 0.30, 0.24, 0.22, 0.15, 0.17, 0.14, 0.18};
int n = price.size();
int budget = 10000; // 千円
double lambda = 0.5;
// 1 単元(100 株)の金額(千円)と、予算の 30% までで買える単元数
std::vector<int> lot(n), cap(n);
for (int i = 0; i < n; ++i) {
lot[i] = price[i] / 10;
cap[i] = 3000 / lot[i];
}
auto x = 0 <= qbpp::int_var("x", n) <= qbpp::array(cap);
qbpp::Expr risk, ret, cost;
for (int i = 0; i < n; ++i) {
double wi = double(lot[i]) / budget;
for (int j = 0; j < n; ++j) {
double wj = double(lot[j]) / budget;
double rho = (i == j) ? 1.0 : (sector[i] == sector[j] ? 0.6 : 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];
}
auto f = risk - lambda * ret + qbpp::cons(9800 <= cost <= budget);
f.simplify_as_binary();
auto sol = qbpp::EasySolver(f).search({{"time_limit", 1.0}});
for (int i = 0; i < n; ++i)
if (sol(x[i]) > 0)
std::printf("%c: %2.0f lots (%4.0f thousand yen)\n", 'A' + i, sol(x[i]),
lot[i] * sol(x[i]));
std::printf("cost = %.0f thousand yen, return = %.2f%%, risk = %.2f%%\n",
sol(cost), 100 * sol(ret), 100 * std::sqrt(sol(risk)));
}
このプログラムは、買う銘柄ごとに単元数と金額(千円)を出力し、最後に合計金額、期待リターン、リスク(標準偏差)を出力します。
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%
xは単元数 $x_i$ の整数変数の配列で、上限capは銘柄ごとに違います。risk、ret、costは、それぞれリスク $V$、期待リターン $R$、合計金額です。EasySolverで 1 秒間探索します。
10 銘柄すべてに分けて買い、期待リターンは 6.98%、リスクは 12.75% になりました。 リスク 12.75% は、どの銘柄のボラティリティ(最も小さい I でも 14%)よりも小さく、分散して買う効果が表れています。 整数計画ソルバーで確かめると、これがこの問題の最適解です。
銘柄数の制限
10 銘柄に少しずつ分けて買うのは管理が大変なので、買う銘柄を 4 つまでに制限します。 銘柄 $i$ を買うとき 1 になるバイナリ変数 $y_i$ を加え、次の制約を課します。
\[\begin{aligned} & x_i \le u_i\, y_i && \text{(すべての銘柄 } i\text{)}\\ & \sum_{i} y_i \le 4 \end{aligned}\]1 つ目の制約により、$y_i = 0$ の銘柄は買えません。 プログラムでは、予算の制約の後に y とこれらの制約を加えます。
#define DOUBLE_TYPE
#include <qbpp/qbpp.hpp>
#include <qbpp/easy_solver.hpp>
#include <cmath>
#include <cstdio>
#include <vector>
int main() {
// 銘柄 A〜J の業種、株価(円)、期待リターン、ボラティリティ(年率)
std::vector<int> sector = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4};
std::vector<int> price = {2450, 6180, 1870, 3320, 1240, 4060, 2890, 5510, 1530, 3970};
std::vector<double> mu = {0.11, 0.10, 0.08, 0.09, 0.07, 0.065, 0.045, 0.05, 0.04, 0.055};
std::vector<double> sigma = {0.32, 0.28, 0.26, 0.30, 0.24, 0.22, 0.15, 0.17, 0.14, 0.18};
int n = price.size();
int budget = 10000; // 千円
double lambda = 0.5;
// 1 単元(100 株)の金額(千円)と、予算の 30% までで買える単元数
std::vector<int> lot(n), cap(n);
for (int i = 0; i < n; ++i) {
lot[i] = price[i] / 10;
cap[i] = 3000 / lot[i];
}
auto x = 0 <= qbpp::int_var("x", n) <= qbpp::array(cap);
qbpp::Expr risk, ret, cost;
for (int i = 0; i < n; ++i) {
double wi = double(lot[i]) / budget;
for (int j = 0; j < n; ++j) {
double wj = double(lot[j]) / budget;
double rho = (i == j) ? 1.0 : (sector[i] == sector[j] ? 0.6 : 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];
}
auto f = risk - lambda * ret + qbpp::cons(9800 <= cost <= budget);
// 買う銘柄は 4 つまで: 銘柄 i を買うとき y[i] = 1
auto y = qbpp::var("y", n);
for (int i = 0; i < n; ++i) f += qbpp::cons(x[i] - cap[i] * y[i] <= 0);
f += qbpp::cons(qbpp::sum(y) <= 4);
f.simplify_as_binary();
auto sol = qbpp::EasySolver(f).search({{"time_limit", 1.0}});
for (int i = 0; i < n; ++i)
if (sol(x[i]) > 0)
std::printf("%c: %2.0f lots (%4.0f thousand yen)\n", 'A' + i, sol(x[i]),
lot[i] * sol(x[i]));
std::printf("cost = %.0f thousand yen, return = %.2f%%, risk = %.2f%%\n",
sol(cost), 100 * sol(ret), 100 * std::sqrt(sol(risk)));
}
このプログラムの出力は次のとおりです。
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.98% から 6.65% に下がり、リスクは 12.75% から 13.19% に上がりました。 これもこの問題の最適解です。
リスクとリターンの関係
$\lambda$ を 0 から 4 まで 0.05 刻みで変えて同じ問題を解き、得られた解のリスクと期待リターンを図にしました。 各点は ABS3 Solver で 1 秒ずつ求めたもので、すべて整数計画ソルバーで最適解であることを確かめています。
- 黒い線は、単元の制約がなく割合を自由に選べる場合の最適解です(効率的フロンティアと呼ばれます)。 同じリスクでこれより高いリターンは得られません。
- 100 株単位の解(青)は黒い線にほぼ重なります。予算に比べて 1 単元の金額が小さいので、単元の制約による損失はわずかです。
- 4 銘柄までに制限した解(橙)は黒い線より右下にあり、同じリターンを得るにはより大きなリスクが必要です。 リターンを重視する右上の端では、もともと少数の銘柄に集中するので、差は小さくなります。
- どの解も、灰色で示した銘柄単独のリスクより小さなリスクで、同じ程度のリターンを得ています。