概要
日本の米国株投資界隈で有名な B氏の投資戦略の最適性に関して考察する。B氏の投資戦略は以下:
- 最初の 1回
- 資産を10等分して、10銘柄を購入する
- 月に 1回 1銘柄を購入
- 購入銘柄は上記 10銘柄のうち、ポートフォリオで時価総額が小さい銘柄
- 購入代金は 給与 + 配当金
10銘柄が金額ベースでバランスするように、月1でリバランスを行うというものだ。売却も行わないし、購入を手控えることもしない。本稿では、上記投資戦略の有用性と問題点を、数理的な直観で考察するとともに JavaScript を使ったシミュレーションでそれを裏付ける
免責と備考:
- 私は金融工学を全く知らない素人
- B氏についてもっと知りたければブログ村の米国株カテゴリに行く
- 彼は本も出版している
- ブログを見に行ってアフィリエイト報酬に貢献するのもよい
- かなり目立つので考察の対象としただけでB氏に特別な感情はない
#00. 考察の範囲
「10銘柄集中投資 + 毎月ありったけ投資」という枠組みは維持したままで、リバランス戦略が妥当であるのかを考察する。例えば以下のトピックは考察の範囲外:
- 選択された10銘柄の妥当性
- 購入の手控え、株式売却など戦略全体の構造の抜本的な部分の是非
#01. 戦略の支持要因
B氏の戦略(以降 B戦略)が特定の条件下では最適な戦略であることを示す。
次のようなケースを考えよう:
- 全銘柄の値動きのモメンタムがなく、ゆらぎのみが存在
この場合、最適な戦略はゆらぎのせいで一番安くなっている銘柄を買うことだ。もし前月のポートフォリオにおいて時価総額ベースでバランスしていたとすれば、今月のポートフォリオにおいて一番時価総額が低くなる銘柄がまさにその銘柄であるので、B戦略は最適となる。実際は前月のポートフォリオはバランスしていないために、B戦略では最適な銘柄を選択できない場合もあろうが、多くの場合最適な銘柄を選択できる
- 最適戦略を外す例
- 前月 A銘柄 9% B銘柄 11%の時価総額とする(ややバランスがずれている)
- 今月 A銘柄の価格 0.8, B銘柄の価格 0.7 とする
- B銘柄を購入するのが最適戦略だが
- 今月の時価総額は A銘柄 7.2% B銘柄 7.7% となるのでA銘柄を選択する
上記の考察、つまりB戦略の最適性は、全銘柄が同一の正のパフォーマンスを持つ場合(全銘柄が一様な割合で値上がりする)や、負のパフォーマンスを持つ場合(全銘柄が一様な割合で値下がりする)にも当てはまる。
調べる価値があるのは、B戦略は 最適戦略をはずすことがたまにあるがその影響はどのようなものか?ということだ。数理的な直観を働かすと、ゆらぎがアホみたいに大きくなければ、市場のモメンタムによらず最適戦略と大して変わらないリターンになるのではないかと考えられる。これについては後でシミュレーションをしよう。
#02. 戦略の不支持要因
前章で考察したように、B戦略はゆらぎが比較的小さい場合には最適戦略を大きく外さないだろうという感触がえられたが、それを覆すような考察をしよう。
前章では銘柄ごとのパフォーマンスが一様であることを仮定したが、実際にはこれは当てはまらない。よくありうる例としては、採用銘柄にクソ株と当たり株が混じるケースがあげられよう。数理的な直観を働かせると、B戦略ではクソ銘柄ばっかり買ってしまうと思われる。
例えば、クソ銘柄が毎月 10%減価して、その他の銘柄の値動きがないとしよう。100の総資産は翌月には99になる。毎月の投入額を1とすると、なんと毎月クソ銘柄を買い付けることになってしまう。この場合、30カ月後にも総資産額は100のままだ。最適戦略はクソ銘柄以外の株を買うことなので30カ月後の総資産はざっと120くらいになるはずだ(クソ銘柄の価値がほぼ0になるので赤字が10、新規投入分が30なので合わせて +20 になるという概算)。毎月ダーツで買う銘柄を決める場合は総資産はざっと117くらいになるはずだ(ダーツで3回クソ銘柄を買ってしまうので赤字額は13で、新規投入分30と合わせて +17となるという概算)。
上記のように、B戦略には相対的に低パフォーマンスな銘柄を買うバイアスがあると言わざるをえない(これについては後にシミュレーションする)。
想定される反論は「10銘柄にはクソ銘柄は含まれていない」「クソ銘柄に見えても長期的にはパフォーマンスはプラスに収束する」というものがあろうが、上記の指摘は「どんな銘柄を厳選したとしても銘柄間のパフォーマンスに差があるであろうことは避けられず、まさにその事実がB戦略の最適性を損ねることになっている」ことを主張するので根が深い。
#03. 配当複利に関する考察
少し寄り道をしよう。B氏の銘柄選定基準は高配当で増配率の高い銘柄である。これらの銘柄はキャッシュフローの多くを配当に分配する。B氏の主張はおそらく以下のようなものであろう:
- 値下がり時に買えば(買える株数が多いので)配当をたくさんもらえる
- 配当を再投資することで複利で効いてくる
- 株価が値下がりしても、企業は減配の決定をしない
- むしろ減配しない銘柄を選ぶ
この主張は、ある程度納得できるものがある。一方、以下のように逆の主張も可能であり、どちらの方が説得的であるかは悩ましい(もし神の手が存在するならば、両者の主張がバランスするのだろう。そうでなければ裁定できてしまうので):
- 配当を出すことは望ましくない(例:バークシャー)
- 配当=利益確定=税金がとられる
- その分を企業が投資に回せば、税金分を繰り延べできる
- 配当がでない分、株価は上がる
銘柄選択に関しては本稿の範囲外であるので、上記の議論は完全に蛇足だ。シミュレーションでは、1株あたりの配当と増配率が一様であることを前提とする
#04. シミュレーション設計
以上で議題がそろった。実際にシミュレーションをしてみよう。
戦略のバリエーション
B戦略の最適性を議論したいのだから、比較対象となる別の戦略が必要となる。以下のように複数の比較対象となる戦略を用意しよう
- S戦略: 買い付け銘柄は完全に事前に順番通りにきめる
- R戦略: 買い付け銘柄は完全にランダムにきめる
- 最適戦略: 神の目線に立ち、最適なリバランスを実施する
上記以外にも、例えば、基準点から株価を測った指標で銘柄を決める、なども考えられるがB氏のパッシブ性を尊重した戦略を採用した。
初期条件
現実的な初期条件で固定したい。以下で固定する
- 初期の総資産は 100
- 月投入額(配当除く)は 1
- 投資期間は 120カ月
- 銘柄の配当は年率 3%, 増配率は年率 10%
- 配当は 3カ月ごと
- 初期の各銘柄の株価を 1 とする
- 総投下資本220 に対する利益率(百分率)でリターンを測る
- 120か月後に時価総額が 250 なら 30 / 220 * 100 = 13.6 がリターン
市場を支配する要因
考察したように以下の要因で銘柄の株価が決定するとする
- ゆらぎ
- ゆらぎの大きさを制御できるようにパラメタ化する
- fvalue と呼び、理論価格からの乖離具合を示す
- 例: fvalue = 0.1 の場合、株価は理論価格の 90%から110% に収まる
- パフォーマンス
- ベータ:市場全体の年率リターン
- アルファ:銘柄ごとに設定される、ベータからのずれ
#05. シミュレーションA
銘柄間のパフォーマンスに差がない、という仮定をおいたシミュレーションを行う。ゆらぎの大きさを「大・中・小」のバリエーション、ベータが「マイナス・ゼロ・プラス」のバリエーションの合わせて9パターンをシミュレーションした結果を以下に示す。
simulationA opt B R S
--------------------------------------------------------------------------------
fval:0, beta: 0 46.34 46.34 46.34 46.34
fval:0.05, beta: 0 53.59 49.93 46.46 46.44
fval:0,10, beta: 0 61.35 55.07 46.47 46.53
fval:0, beta: 0.05 93.34 93.34 93.34 93.34
fval:0.05, beta: 0.05 101.92 97.82 93.62 93.65
fval:0.10, beta: 0.05 110.82 103.61 93.58 93.54
fval:0, beta: -0.05 16.28 16.28 16.28 16.28
fval:0.05, beta: -0.05 22.83 19.44 16.31 16.27
fval:0.10, beta: -0.05 29.72 23.89 16.51 16.39
予想通り、B戦略は最適戦略とまではいかないものの、ほかの戦略と比較しても優位であることがわかる。ゆらぎが大きくなるほどこの傾向は顕著になる。これは #01 で概算した結果と整合的だ。
一つ、面白い現象が見えていることを指摘しよう。初期配置では、配当は3%である。そしてベータが -5% である。直観的に考えると、投資を続けるほど損失が増え、赤字になると考えがちだがそうではない(ベータがマイナスのケースを見よ)。なぜなら株価が下落するに従い、配当がどんどん上昇していき、どこかのタイミングで配当額が時価総額の損失を打ち消してくれるようになるからだ。
具体的な数字を上げよう。5年後には株価は 0.95^5 ≒ 0.77 になる。したがって同じ資金で 1/0.77 ≒ 1.3 倍の株を購入できる。それに加え 増配率を考えると、1.3 * 0.03 * (1 + 0.1)^5 ≒ 7.7 % の配当利回りとなる。これはベータを上回る。減配しない銘柄・増配率の高い銘柄の強力さを実感できる
#06. シミュレーションB
ゆらぎがない、という仮定をおいたシミュレーションを行う。銘柄間のパフォーマンスの差が「大・中・小」のバリエーション、ベータが「マイナス・ゼロ・プラス」のバリエーションの合わせて9パターンをシミュレーションした結果を以下に示す。
simulationB opt B R S
--------------------------------------------------------------------------------
beta: 0, alpha: 0.1 x1 ,-0,1 x1 95.58 48.61 53.58 53.54
beta: 0, alpha: 0.1 x2 ,-0,1 x2 99.78 50.67 60.57 60.76
beta: 0, alpha: 0.1 x3 ,-0,1 x3 103.98 53.13 68.40 68.00
beta: 0.05, alpha: 0.1 x1 ,-0,1 x1 161.39 95.47 102.67 102.86
beta: 0.05, alpha: 0.1 x2 ,-0,1 x2 167.55 95.79 112.64 112.40
beta: 0.05, alpha: 0.1 x3 ,-0,1 x3 173.71 96.96 122.38 121.94
beta: -0.05, alpha: 0.1 x1 ,-0,1 x1 49.74 20.04 22.30 22.09
beta: -0.05, alpha: 0.1 x2 ,-0,1 x2 52.56 23.40 27.68 27.96
beta: -0.05, alpha: 0.1 x3 ,-0,1 x3 55.37 27.03 34.16 33.86
予想通り、ベータの数値によらず、B戦略はどの戦略と比較しても低いパフォーマンスしか上げられていない(なお、上昇相場=ベータがプラスの場合のほうがその差が顕著になっている)。これは #02 で概算したように、B戦略ではパフォーマンスが劣る銘柄を買い付けやすくなってしまうためだ。たった 1,2 銘柄におけるパフォーマンスの差分で、B戦略の欠点が観測できるほど大きくなる。
#07. シミュレーションC
これまでの結果では「ゆらぎがある場合はB戦略は結構イケてる」、「アルファがある場合はB戦略はイケてない」ことがわかった。じゃあ両方混ぜたらどうだろう?現実的と思われるいくつかの設定でシミュレーションをした結果は以下の通りとなる。
simulationC opt B R S
--------------------------------------------------------------------------------
fval: 0.01 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1 161.47 95.49 103.27 102.83
fval: 0.05 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1 162.11 99.85 102.96 103.35
fval: 0.10 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1 164.79 105.93 103.42 103.47
fval: 0.01 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1 49.79 20.03 22.25 22.11
fval: 0.05 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1 50.54 22.88 21.91 22.08
fval: 0.10 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1 53.04 28.43 23.10 22.84
fval: 0.01 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2 168.49 96.74 112.35 112.38
fval: 0.05 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2 172.62 101.85 112.79 112.96
fval: 0.10 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2 178.80 108.18 113.02 113.26
パラメタ次第だとは思うのだけれど、色々いじくった結果としては、「B戦略は最適な戦略ではない」と結論付けられるように思われる。
#08. 一旦まとめ
B氏のリバランス戦略の二つの側面を明らかにし、シミュレーションで実証した
- 側面1: ゆらぎ効果によるタイミング投資
- 時価総額ベースで最小の銘柄を選択するということは、すなわち「この銘柄はたまたま下がっているだけで今買えばあとで報われる」と信じるようなものだ。もしそのように価格形成がされた場合、非常にうまくいく。
- 側面2: 選定銘柄間のパフォーマンス差を粗視化
- 選定銘柄間に有意なパフォーマンス差が存在する場合、長期的・慢性的に時価総額でのバランスを崩す要因となる。それを是正しようとするリバランスは、劣後したパフォーマンスの銘柄を嗜好することと同義だ。そのような銘柄は相対的に配当が高くなるという効果があるものの、資産の最大化の観点では正当化できない。にもかかわらずこのリバランス戦略をとるということは、選定銘柄間のパフォーマンス差を意図的に無視していると言わざるを得ない。
上述の二つの側面のうち(私の興味があるパラメータ領域では少なくとも)後者が支配的であるため、B氏のリバランス戦略を採用する積極的な意義を見出すことができなかった。むしろ毎月ダーツやサイコロなどで投資先をランダムに決めるようなリバランス戦略の方が総じてパフォーマンスは高かった。
#09. 私の主観を交えたまとめ
私の主観を交えてB氏の投資戦略全体に対する意見・立場を明確にしたいと思う。
私は、基本的にはB氏の投資スタンスに共鳴をする立場だ(でなければこんな長々と記事を書かない)。それは主に以下のような基礎概念を共有していると思われるからだ
- 中長期的には株式投資が一番報われる資産運用だ
- バフェット・ピケティ・マルキールや過去のS&Pのチャートで実感できる
- タイミング投資・アクティブ投資をせずに機械的に投資するべきだ
- 底も天井も素人には予測できない
- 市場平均程度のリターンでも十分早く資産形成できる
- 増配銘柄は相場下落時にこそ儲けのチャンス
- 悲観・絶望が支配する時に投資スタンスを貫きやすい
だからこそ、B氏の投資戦略のリバランス戦略に違和感を持った。つまり、B戦略には機械的な投資という性質よりも、「タイミング投資的な色彩」や「銘柄選択に対するある種の自身=アクティブな側面」があったからだ。これらに関しては本文を読んだ方には同意を得られるだろう。これなら、S戦略やR戦略のように、機械的に追加投資先を決めたほうが一貫性があるのではないだろうか?本稿はそれをある程度定量的に示すことができたと考える。
#10. ソースコード
バグがないといいなあ。Node v11 で動作確認。
const N = 10; // number of stocks
const T = 120; // number of months to invest
const DIV = 0.03; // dividend
const GDIV = 0.1; // growth ratio of dividend.
// utility
const range = (start, end) => [...Array(end - start).keys()].map(x => x + start);
const sum = arr => arr.reduce((a, x) => a + x, 0);
// convert yearly return to monthly/quarterly return
const y2m = r => Math.E ** (Math.log(1 + r) / 12) - 1;
const y2q = r => Math.E ** (Math.log(1 + r) / 4) - 1;
// stock prices
function priceTableOf (fvalue, rate) {
const prices = [1.0];
range(1, T).forEach(t => prices.push(prices[t - 1] * (1 + y2m(rate))));
return prices.map(p => p * (1 + (2 * Math.random() - 1) * fvalue));
}
// policies
function policyB (shares, prices) {
return shares.reduce((a, s, n) => shares[a] * prices[a] < shares[n] * prices[n] ? a : n, 0);
}
function policyR (shares, prices) {
return Math.floor(Math.random() * shares.length);
}
const policyS = (function () {
let idx = -1;
return function (shares, prices) {
idx = (idx + 1) % shares.length;
return idx;
};
})();
// simulator: returns average of totalReturn for each policy
function simulation (nSample, fvalue, beta, alphaArr, policies) {
const result = Array(policies.length + 1).fill(0); // +1 is for optimal policy
for (let i = 0; i < nSample; i++) {
doSimulation(fvalue, beta, alphaArr, policies).forEach((x, i) => {
result[i] += x;
});
}
return result.map(x => x / nSample);
}
function doSimulation (fvalue, beta, alphaArr, policies) {
const prices = range(0, N).map(n => priceTableOf(fvalue, beta + alphaArr[n]));
// create optimal policy
const optimalPolicy = (_, __, t) => {
const values = prices.map((_, n) => { // value = price + yield
let value = prices[n][T - 1] / prices[n][t]; // now: 1 -> end: 1.X
// XXX: dividened
return value;
});
return values.reduce((a, x, i) => values[a] < values[i] ? i : a, 0);
};
const result = [optimalPolicy, ...policies].map(policy => {
const shares = Array(N).fill(100 / N);
range(1, T).forEach(t => {
let newMoney = 1.0; // salary
if (t % 3 === 2) { // dividend
const ratio = (1 + y2q(GDIV)) ** ((t - 2) / 3);
newMoney += sum(shares) * y2q(DIV) * ratio;
}
const targetIdx = policy(shares, range(0, N).map(n => prices[n][t]), t);
shares[targetIdx] += newMoney / prices[targetIdx][t];
});
const total = shares.reduce((a, s, n) => a + s * prices[n][T - 1], 0);
const totalReturn = (total - 100 - T) / (100 + T) * 100;
return totalReturn;
});
return result;
}
const nSample = 100;
const policies = [policyB, policyR, policyS];
function printHeader (header) {
const disp = ['opt', 'B', 'R', 'S'].map(s => s.padEnd(7, ' ')).join('');
console.log(header.padEnd(50, ' '), disp);
console.log('-'.repeat(80));
}
// simulationA
const simA = ([fval, beta]) => simulation(nSample, fval, beta, Array(N).fill(0), policies);
const resultA = [
[0.0, 0], [0.05, 0], [0.10, 0],
[0.0, 0.05], [0.05, 0.05], [0.10, 0.05],
[0.0, -0.05], [0.05, -0.05], [0.10, -0.05]
].map(simA);
const labelsA = [
'fval:0, beta: 0',
'fval:0.05, beta: 0',
'fval:0,10, beta: 0',
'fval:0, beta: 0.05',
'fval:0.05, beta: 0.05',
'fval:0.10, beta: 0.05',
'fval:0, beta: -0.05',
'fval:0.05, beta: -0.05',
'fval:0.10, beta: -0.05'
];
printHeader('simulationA');
resultA.forEach((r, i) => console.log(labelsA[i].padEnd(50, ' ') + r.map(x => x.toFixed(2).padStart(7, ' ')).join('')));
return;
// simulationB
const simB = ([beta, alphaArr]) => simulation(nSample, 0, beta, alphaArr, policies);
const aArr = Array(N).fill(0);
aArr[0] = 0.1;
aArr[aArr.length - 1] = -0.1;
const aArr2 = [...aArr];
aArr2[1] = 0.1;
aArr2[aArr2.length - 2] = -0.1;
const aArr3 = [...aArr2];
aArr3[2] = 0.1;
aArr3[aArr3.length - 3] = -0.1;
const resultB = [
[0.0, aArr], [0.0, aArr2], [0.0, aArr3],
[0.05, aArr], [0.05, aArr2], [0.05, aArr3],
[-0.05, aArr], [-0.05, aArr2], [-0.05, aArr3]
].map(simB);
const labelsB = [
'beta: 0, alpha: 0.1 x1 ,-0,1 x1',
'beta: 0, alpha: 0.1 x2 ,-0,1 x2',
'beta: 0, alpha: 0.1 x3 ,-0,1 x3',
'beta: 0.05, alpha: 0.1 x1 ,-0,1 x1',
'beta: 0.05, alpha: 0.1 x2 ,-0,1 x2',
'beta: 0.05, alpha: 0.1 x3 ,-0,1 x3',
'beta: -0.05, alpha: 0.1 x1 ,-0,1 x1',
'beta: -0.05, alpha: 0.1 x2 ,-0,1 x2',
'beta: -0.05, alpha: 0.1 x3 ,-0,1 x3'
];
console.log();
printHeader('simulationB');
resultB.forEach((r, i) => console.log(labelsB[i].padEnd(50, ' ') + r.map(x => x.toFixed(2).padStart(7, ' ')).join('')));
// simulationC
const simC = ([fval, beta, alphaArr]) => simulation(nSample, fval, beta, alphaArr, policies);
const resultC = [
[0.01, 0.05, aArr], [0.05, 0.05, aArr], [0.10, 0.05, aArr],
[0.01, -0.05, aArr], [0.05, -0.05, aArr], [0.10, -0.05, aArr],
[0.01, 0.05, aArr2], [0.05, 0.05, aArr2], [0.10, 0.05, aArr2]
].map(simC);
const labelsC = [
'fval: 0.01 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.05 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.10 beta: 0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.01 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.05 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.10 beta: -0.05, alpha: 0.1 x1 ,-0,1 x1',
'fval: 0.01 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2',
'fval: 0.05 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2',
'fval: 0.10 beta: -0.05, alpha: 0.1 x2 ,-0,1 x2'
];
console.log();
printHeader('simulationC');
resultC.forEach((r, i) => console.log(labelsC[i].padEnd(50, ' ') + r.map(x => x.toFixed(2).padStart(7, ' ')).join('')));