scipy.optimize.minimizeで積立投信のリバランスをする

積立投信を始めて3年になる。

国内債券、外国債券、国内株式、外国株式(先進国)、外国株式(新興国)の5つのインデックスファンドに決まった割合で毎月投資してるんだけど、時間経過とコロナでの乱高下の影響もあり、ポートフォリオが崩れてきた。これをリバランスして適切な割合にしたい。

リバランス用に潤沢な資金があれば簡単な計算でできるんだけど、そんな資金はなく毎月の投資額の中で、いつもと割合を変えてやりたい。となると話がややこしくなる。

種類 目標比率 保有
国内債券 0.3 28万円
国債 0.1 11万円
国内株式 0.1 12万円
外国株式(先進国) 0.25 30万円
外国株式(新興国 0.25 24万円
合計 1.0 105万円

このケースで5万でリバランスしたいとする。すると110万円で目標比率にしたいので

種類 目標比率 目標保有 保有額との差
国内債券 0.3 33万円 5万円
国債 0.1 11万円 0万円
国内株式 0.1 11万円 -1万円
外国株式(先進国) 0.25 27.5万円 -2.5万円
外国株式(新興国 0.25 27.5万円 3.5万円
合計 1.0 110万円 5万円

マイナスが出てしまう。

売却すると税金かかるしそんな面倒なことはしたくないので、1ヶ月でリバランスするのは諦めて複数月かけて徐々に目標比率に近づけていきたい。

しかしどういう比率で買っていけばいいのかが分からない。上記の場合、国内株式と外国株式(先進国)を売らない代わりに国内債券と外国株式(新興国)の購入額を減らせばいいんだろうけど、具体的にはいくら買えばいいのか、簡単な計算では分からない。

しかもややこしいことにつみたてNISAもやっているので、その縛りがある。つみたてNISAの枠は使い切りたいが、購入できるのは株式だけ。

つまりお前は何をしたいのか

  • それぞれの商品の投資額は0円以上
  • 全商品の投資額の合計がy円
  • 国内株式、外国株式(先進国)、外国株式(新興国)の投資額の合計がx円

以上を満たした上で、理想のポートフォリオに一番近付く投資額を決める。

「理想のポートフォリオ」との差は、上記目標保有額と、投資後の実際の保有額との2乗誤差の和で定義すればよい(絶対値誤差でもいいんだが)。

さて、これで問題の定式化はできたので、これを scipy.optimize.minimize で解くぞ

import numpy as np
from scipy.optimize import minimize

ratio = np.array([0.3, 0.1, 0.1, 0.25, 0.25])  # 目標比率
current = np.array([*****, *****, *****, *****, *****])  # 現保有額(ないしょ)
amount = *****  # 月の投資額(これもないしょ)

# 目的関数の生成
def generate_f(ratio, current, amount):
    def f(xs):
        goal = (sum(current) + amount) * ratio

        # 10000で割るのは最適化エラーを防ぐためのワークアラウンド
        return sum((current + xs - goal) ** 2) / 10000
    return f

f = generate_f(ratio, current, amount)

# 制約条件

# 各商品への投資額は0以上「月の投資額」以下
bounds = [(0, amount) for _ in range(5)]
# 全商品への投資額の合計が「月の投資額」に等しい
# つみたてNISA商品への投資額の合計が目標額(33300円)に等しい
cons = ({'type': 'eq', 'fun': lambda xs: sum(xs) - amount},
        {'type': 'eq', 'fun': lambda xs: sum(xs[2:]) - 33300})

# initialは何でもいいけど、とりあえず均等配分しておく
x0 = np.array([amount / len(ratio) for _ in ratio])

# 最適化!
result = minimize(f, x0, bounds=bounds, constraints=cons)

# 0なら成功
print(result.status)
print(result.x)

(つみたてNISAは年額40万円までなので400000 / 12 ≒ 33300円を毎月積み立てている)

ここまでやるなら買付まで自動でやりたいが、残念ながらAPIがないので無理だった。残念。