ポートフォリオ最適化

予算 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 株単位、4 銘柄まで、銘柄単独)

  • 黒い線は、単元の制約がなく割合を自由に選べる場合の最適解です(効率的フロンティアと呼ばれます)。 同じリスクでこれより高いリターンは得られません。
  • 100 株単位の解(青)は黒い線にほぼ重なります。予算に比べて 1 単元の金額が小さいので、単元の制約による損失はわずかです。
  • 4 銘柄までに制限した解(橙)は黒い線より右下にあり、同じリターンを得るにはより大きなリスクが必要です。 リターンを重視する右上の端では、もともと少数の銘柄に集中するので、差は小さくなります。
  • どの解も、灰色で示した銘柄単独のリスクより小さなリスクで、同じ程度のリターンを得ています。

Back to top

Page last modified: 2026.10.09.

© 2026 中野浩嗣, 広島大学