 | レベル: 上級 木村 桂 (kimuc@jp.ibm.com), ソフトウェア事業, IBM
2006年 3月 03日 4回目となる本連載では MACD を扱います。数あるテクニカル指標の中でも比較的精度が高く、実際に参考されながら売買判断を行っている方も多い、いわば「本命」の登場です。 今回はこの MACD を用いたアルゴリズムを紹介するとともに、MACD そのものの考え方を少し改良して、オリジナルのテクニカル分析アルゴリズムを作ってみることにします。
なお、カブロボ SDK のライブラリでも標準機能として MACD を扱うことは可能ですが、今回はアルゴリズムを改良する目的もありますから、この標準機能は使いません。すべてスクラッチからプログラムコードを記述していきます。そのため、MACD アルゴリズムの勉強にもなると思います。
MACD(Moving Average Convergence/Divergence)
まずこの考え方自体が非常に興味深く、また利にかなった内容になっていることを理解していただくために、MACD(「マックディー」と発音するそうです)の基本的な考え方について簡単に説明しておきます。
MACD では2つの移動平均値を用います。ただしここで用いる移動平均は単純な加重平均(MA: Moving Average)ではなく、指数平滑平均(EMA: Exponential Moving Average)という計算を行います。
通常な移動平均では、各サンプルは全て平等に重み付けがなされ、単純に全ての値を加算しサンプル数で割ることで平均値を求めます。一方、EMA では各サンプルの重要度を平等に扱いません。株価などはまさにうってつけの例ですが、例えば10日前の株価と前日の株価では、明日の株価動向を推測するという目的においては参考になる度合いが異なります。10日前の株価がいくらであったとしても、その間に大暴落や大暴騰があって価格が大きく変化した場合、明日以降の株価を推測する際により参考になるのは直前の株価であることに疑問はないと思われます。 にも関わらず平均値を求める際に10日前の株価と1日前の株価を平等に扱うのは本来ふさわしくないようにも思えます。
平均値: MA = ( V1 + V2 + .... + Vn ) / n
※Vk : n日間の株価( k = 1 ~ n)
そのような考え方から、EMA では平均値を求める際のサンプルとなる株価の扱いをあえて不平等にしています。具体的には直前の株価のみ、その重みを他の倍にしています。これによって直前の株価だけは EMA を求める際の関わり度合いが他の倍になっている、ということになります。直近の最も新しいデータのみを最重要視した上で平均値を計算することになります。また事実上サンプルを1つ余計に加算していることなるため、分母はサンプル数に1を加えたものになります。
指数平滑平均値: EMA = ( V1 + V2 + .... + Vn + Vn ) / ( n + 1 )
※Vk : n日間の株価( k = 1 ~ n)
MACD の話に戻ります。MACD では短期と長期、2つの EMA を計算します。一般的にはそれぞれ 12 日間と 26 日間を使う場合が多いようですがこの数字を多少前後に調整することは問題ないと思われます。ただ通常の移動平均を用いてゴールデンクロスやデッドクロスを測る場合と比べてかなり短期間のデータを使うことになります。そして MACD の値を次の式で求めます:
MACD = 短期EMA - 長期EMA
※値を正規化するケースでは
MACD = ( 短期EMA - 長期EMA ) / 直近の株価
とすることもあります。今回のプログラムでは異なる銘柄同士で MACD の値を比較するため、正規化する後者の計算式を使います。
この式からも分かるように、MACD の値は短期EMA と長期EMA の差です。したがって、MACD がゼロになる瞬間というのは短期EMA と長期EMA が重なる瞬間ですからゴールデンクロスかデッドクロスを意味しています。MACD がマイナスからプラスに向かってゼロになるのはゴールデンクロスで、プラスからマイナスに向かってゼロになるのはデッドクロスを意味します。 このことから、MACD の値の推移(特に値がプラスなのかマイナスなのか、そしてゼロをまたぐ瞬間はいつか)を見ているだけでも移動平均と同等の売買タイミングの参考になるものと思われます。
ただ一般的に MACD と呼ばれる指標チャートを見ると、更に「シグナル」と呼ばれるもう1つのテクニカル指標を併用することが多いです。このシグナルは MACD の値を更に単純移動平均化した数値です。
SIGNAL = ( M1 + M2 + ... + Mt ) / t
※Mk : t 日間の MACD 値( k = 1 ~ t)
一般的に紹介されている MACD という指標では、これらの MACD 値と SIGNAL 値の2つの推移を見て、売買ポイントとしてのゴールデンクロス/デッドクロスを探す、というものがほとんどです。したがって更に MACD2(「マックディーツー」)とよばれる値を以下の式で計算して、この値がマイナスからプラスになる瞬間を「買い」のシグナル、プラスからマイナスになる瞬間を「売り」のシグナルとみなす方法が多く紹介されています。
MACD2 = MACD - SIGNAL
アルゴリズム
以上が一般的に使われている MACD というテクニカル指標の考え方です。
まずはこの MACD を普通に(?)使って、以下のようなアルゴリズムでカブロボを実装することにします。MACD や MACD2 の計算そのものが少し複雑ですが、ロボットの基本的な投資方法は今までと全く変わりません。 売買するのに相応しい銘柄を1日に1つ特定し、1売買単位だけ注文するだけです。
- まず前営業日に注文がなされていた場合は、無条件にその決済注文を行う(つまり注文の翌日に決済する)。
- 各銘柄ごとに充分な日数分※の株価終値を取得し、そこから短期EMA、および長期EMA を求める
- 2. で求めた値を基にして、MACD 値を求める
- 3. で求めた値を基にして、SIGNAL 値を求める
- 更に 2. と 3. で求めた値を使い、MACD2 値を求める
- 全銘柄でこの MACD2 の計算を行った値を比較し、最も MACD2 値の絶対値が小さな銘柄を売買対象として特定します。
(MACD2 の絶対値が小さい = MACD と SIGNAL のゴールデンクロス/デッドクロスが近い)
- MACD2 値の絶対値が小さな銘柄を求めて、MACD 値がプラスだった場合はこの後下がっていくことが予想されるので売り注文、MACD 値がマイナスだった場合はこの後上がっていくことが予想されるので買い注文とする。注文単位は1売買単位とする。
※最終的に MACD2 を求めるには ( 長期 EMA 計算の日数 + シグナル計算の日数 ) 分だけのサンプルがあらかじめ必要になります。
Java のプログラムで実装
ここまでに決めたアルゴリズムを実際の Java プログラムで記述してみたのが、以下のソースコードになります。ここでは run メソッドの中身と、EMA 値と MACD 値、そして SIGNAL 値を求めるためのプライベート関数の内容を紹介しています:
public void run(InvestmentAgent arg0) {
// TODO Auto-generated method stub
int EMA1DAY = 12; // ここで指定した日数分のサンプルを用いて平滑移動平均1を求める
int EMA2DAY = 26; // ここで指定した日数分のサンプルを用いて平滑移動平均2を求める
int SIGDAY = 9; // ここで指定した日数分のサンプルを用いてシグナルを求める
Stock[] stocks = arg0.getStocks(); // 銘柄リストの取得
int N = stocks.length; // 銘柄リスト数(今回のルールでは常に 100 のはず)
double minmacd2;
int i, j, minidx, tradetype = 0;
//. ポートフォリオを取得
InformationManager infoManager = arg0.getInformationManager();
Portfolio portfolio = arg0.getPortfolio();
Map holdingMap = portfolio.getHoldings();
// 前営業日に注文した株があれば決済する
if( holdingMap.size() > 0 ){
//. 前営業日に注文した銘柄の情報を取得
Iterator itr = holdingMap.values().iterator();
Holding holding = ( Holding )itr.next();
// 決済なので、注文した内容が買いであれば売り、売りであれば買いの注文を行う
if( holding.getNumber() > 0 ){
tradetype = StockOrder.SELL;
}else{
tradetype = StockOrder.BUY;
}
// 決済注文
SimpleStockOrder stockOrder = new SimpleStockOrder();
stockOrder.setStock( holding.getStock() ); // 注文の銘柄を設定
stockOrder.setLimitType( StockOrder.MARKET ) // 成り行き注文
stockOrder.setTradeType( tradetype ); // 買い/売り注文を設定
stockOrder.setQuantity( 1 ); // 注文を1単位株に設定
arg0.order( stockOrder ); // 注文の発行
}
// 各銘柄について MACD と SIGNAL を求める。s
Calendar cdt = Time.getTime(); // 当日の日付
for( i = 0, minidx = -1, minmacd2 = 100.0; i < N; i ++ ){
int arrsize = EMA2DAY + SIGDAY; // 最終的な MACD2 の計算に必要なサンプル数
long cp[] = new long[arrsize];
// 直近の日付から RCIDAY 日分の銘柄指標データを取得
List list = infoManager.getIndexInformation( stocks[i], cdt, -1 * arrsize );
for( j = 0; j < arrsize; j ++ ){
// 配列の後半になるほど最近のデータ
IndexInformation indexInformation = ( IndexInformation )list.get( j );
cp[j] = indexInformation.getClosingPrice(); // ( arrsize - j )日前の終値
}
double macds[] = getMACDs( cp, EMA1DAY, EMA2DAY );
double macd = macds[SIGDAY-1]; // この銘柄の MACD
double sig = getSignal( macds, SIGDAY ); // この銘柄の Signal
double macd2 = macd - sig; // この銘柄の MACD2
// System.out.println( stocks[i].getName() + "(" + stocks[i].getCode() + ")\tMACD2: " + macd2 );
//. MACD2 の絶対値がもっともゼロに近い銘柄を探す
if( Math.abs( macd2 ) < minmacd2 ){
minmacd2 = Math.abs( macd2 );
tradetype = ( ( macd >= 0 ) ? StockOrder.SELL : StockOrder.BUY );
minidx = i;
}
}
if( minidx > -1 ){
System.out.println( "注文銘柄: " + stocks[minidx].getName()
+ "(" + stocks[minidx].getCode() + ")\tMACD2: " + minmacd2 );
SimpleStockOrder stockOrder = new SimpleStockOrder();
stockOrder.setStock( stocks[minidx] ); // 注文の銘柄を設定
stockOrder.setLimitType( StockOrder.MARKET ); // 成り行き注文
stockOrder.setTradeType( tradetype ); // 買い/売り注文を設定
stockOrder.setQuantity( 1 ); // 注文を1単位株に設定
arg0.order( stockOrder ); // 注文の発行
}
}
// 数日分の MACD 値を求める
private double[] getMACDs( long[] v, int ema1day, int ema2day ){
int l = v.length;
int ema1len = ( l - ema1day + 1 ), ema2len = ( l - ema2day + 1 );
double ema1[] = new double[ema1len];
double ema2[] = new double[ema2len];
double macds[] = new double[ema2len];
// 短期 EMA を求める
for( int i = 0; i < ema1len; i ++ ){
ema1[i] = getEMA( v, i, ema1day );
}
// 長期 EMA を求める
for( int i = 0; i < ema2len; i ++ ){
ema2[i] = getEMA( v, i, ema2day );
}
// MACD ( =短期EMA-長期EMA )を求める
for( int i = 0; i < ema2len; i ++ ){
macds[i] = ema1[i+ema2day-ema1day] - ema2[i];
// 本来の計算はここまででもいいのだが、
// 後で数値の大小比較をするため、直近値で割って正規化しておく
macds[i] = macds[i] / v[ema2day-1];
}
return macds;
}
// 平均値(MACD 値の移動平均値を計算して SIGNAL 値を求める)
private double getSignal( double[] macds, int sigday ){
double sum = 0.0;
for( int i = 0; i < sigday; i ++ ){
sum += macds[i];
}
return ( sum / sigday );
}
// 平滑平均値
private double getEMA( long[] v, int startidx, int num ){
int n = startidx + num - 1;
double ema;
double sum = 0.0;
for( int i = startidx; i <= n; i ++ ){
sum += ( double )v[i];
}
sum += ( double )v[n];
ema = ( sum / ( num + 1 ) );
return ema;
} |
何度も繰り返して平均値や指数平滑平均値を求めているため複雑に見えますが、要は上記で説明した内容をロジック化しただけです。MACD2 を求めて、もっともゴールデンクロス/デッドクロスが近そうな銘柄を探して、その流れに乗るような売買を行います。
シミュレーション結果
このプログラムを実行すると以下のような結果になります。シミュレート期間は 2005 年の4月です:
■1日目2005/04/01 金 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 5,000,000円
手持ち資金(運転可能金額 + ロック金額) = 5,000,000円
運転可能金額 = 5,000,000円
ロック資金 = 0円
単純計算年率利益率 0.0%
○ロボットの銘柄評価
注文銘柄: キヤノン(7751) MACD2: 1.311853163704314E-4
売却: 7751 キヤノン 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
キヤノン| 7751| -100| 5,740| 30
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
キヤノン| 7751| -100| 5,740| 30
実行時間: 1.844 秒.
■2日目2005/04/02 土 終了時の所持金額
No Data
実行時間: 0.0 秒.
■3日目2005/04/03 日 終了時の所持金額
No Data
実行時間: 0.0 秒.
■4日目2005/04/04 月 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 5,002,000円
手持ち資金(運転可能金額 + ロック金額) = 5,574,000円
運転可能金額 = 4,430,000円
ロック資金 = 1,144,000円
単純計算年率利益率 14.0%
○ロボットの銘柄評価
購入: 7751 キヤノン 1単位 成行
注文銘柄: ブリヂストン(5108) MACD2: 1.5723821237271368E-4
購入: 5108 ブリヂストン 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
キヤノン| 7751| 100| 5,740| -80
ブリヂストン| 5108| 1000| 1,998| -37
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
ブリヂストン| 5108| 1000| 1,998| -37
実行時間: 1.0 秒.
:
:
(略)
:
:
■28日目2005/04/28 木 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 5,056,300円
手持ち資金(運転可能金額 + ロック金額) = 3,669,300円
運転可能金額 = 3,669,300円
ロック資金 = 0円
単純計算年率利益率 21.0%
○ロボットの銘柄評価
売却: 9064 ヤマトHD 1単位 成行
注文銘柄: エーザイ(4523) MACD2: 6.858710562412593E-5
購入: 4523 エーザイ 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
ヤマトHD| 9064| -1000| 1,387| -2
エーザイ| 4523| 100| 3,470| 0
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
エーザイ| 4523| 100| 3,470| 0
実行時間: 1.625 秒.
■29日目2005/04/29 金 終了時の所持金額
No Data
実行時間: 0.0 秒.
■30日目2005/04/30 土 終了時の所持金額
No Data
実行時間: 0.0 秒.
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
【最終結果】
あなたのロボットによる、該当期間の運用成績は、こちらです。
総資産 = 手持ち資金+所有株式時価総額 = 5,059,300円
単純計算年率利益率 21.0%
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
もちろん偶然の要素が多分にあると思いますが、MACD の精度の高さを示す結果になりました。一ヶ月の運用の結果、資産はこの連載最高の 59,300 円も増えました。単純計算した年利も 21% となり、ホクホクです。
MACD の改良
以下では私自身の考えを紹介しています。一般的に紹介されている MACD の考え方や計算とは異なることをあらかじめご了承ください。
今回、MACD を利用するにあたりカブロボの標準ライブラリを使わず全てスクラッチから作成した理由は「改良するため」でした。上記 MACD の説明の中で EMA という平滑平均の考え方を取り入れている、と紹介しましたが、この部分は少し改良の余地があるように思います。
というのも、平滑平均を用いる理由は「最近のデータをより重要視するため」であり、この考え方自体は非常に利にかなっていると思われます。 しかし実際の計算式においては「直近の1つのデータだけを特別視して加重」して平均値を求めました。
つまり10日間の EMA を求める場合、1日前のデータだけは確かに特別扱いされますが、10日前のデータと2日前のデータは全く同じ扱いになるのです。100日間の EMA では1日前のデータだけを特別視するが、100日前のデータと2日前のデータは同じ扱いになる、ということです。これでは「最近のデータをより重要視する」とはいえないようにも感じられます。
そこで平滑平均の定義を自分なりに少し変更する、ということに挑戦してみました。具体的には平滑平均を以下のような定義で考えることにします:
- 最近に近いデータほど重要度が高いとみなし、平均値計算時の重みを増やす
- 例えば10日間の例であれば、10日前のデータは1回だけ加算、9日前のデータは2回加算、以後繰り返して、1日前のデータは10回加算して、( 1 + 2 + .. + 10 = )55 で割ったものを改良後の平滑平均値とする
これを式であらわすと以下のようになります:
今までの EMA EMA = ( V1 + V2 + .... + Vn + Vn ) / ( n + 1 )
改良後の EMA EMA = ( V1 + V2 + V2 + V3 + V3 + V3 + .... + Vn + Vn + ...+ Vn ) / { n × ( n + 1 ) ÷ 2 } ※Vk : n日間の株価( k = 1 ~ n)
再シミュレート
この EMA を用いて再度アルゴリズム化して、シミュレートを試みてみましょう。
まずプログラムですが、getEMA 関数を以下のように変更します。実は余分なコードが含まれていますが、origEMA という変数の値を true にすれば従来の EMA で、false にすれば改良後の EMA で計算するようにした結果の冗長部分です。再コンパイルは必要ですが、この変数の値を書き換えるだけで改良前後のシミュレートを簡単に切り替えることができるようにしました。
// 平滑平均値
private double getEMA( long[] v, int startidx, int num ){
// true : 通常の EMA で計算、 false : 改良した EMA で計算
boolean origEMA = false;
int n = startidx + num - 1;
double ema;
double sum = 0.0;
for( int i = startidx; i <= n; i ++ ){
if( origEMA ){
// 通常の EMA は普通に1回ずつ足していく
sum += ( double )v[i];
}else{
// 改良版では最近のデータほど多い回数だけ加算する
for( int j = startidx; j <= i; j ++ ){
sum += ( double )v[i];
}
}
}
if( origEMA ){
// 通常の EMA は直近データのみ1回多く加算し、その平均値を求める
sum += ( double )v[n];
ema = ( sum / ( num + 1 ) );
}else{
// 改良版では加算したサンプルの数の合計を求めて、その数で割る
int m = ( int )( n * ( n + 1 ) / 2 );
ema = ( sum / m );
}
return ema;
}
|
そして origEMA = false と改良して、再度シミュレートした結果は以下のようになりました。
■1日目2005/04/01 金 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 5,000,000円
手持ち資金(運転可能金額 + ロック金額) = 5,000,000円
運転可能金額 = 5,000,000円
ロック資金 = 0円
単純計算年率利益率 0.0%
○ロボットの銘柄評価
注文銘柄: 丸井(8252) MACD2: 0.14046168968688744
購入: 8252 丸井 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
丸井| 8252| 100| 1,454| 27
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
丸井| 8252| 100| 1,454| 27
実行時間: 1.829 秒.
■2日目2005/04/02 土 終了時の所持金額
No Data
実行時間: 0.0 秒.
■3日目2005/04/03 日 終了時の所持金額
No Data
実行時間: 0.0 秒.
■4日目2005/04/04 月 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 4,997,000円
手持ち資金(運転可能金額 + ロック金額) = 4,854,600円
運転可能金額 = 4,854,600円
ロック資金 = 0円
単純計算年率利益率 -21.0%
○ロボットの銘柄評価
売却: 8252 丸井 1単位 成行
注文銘柄: 日通(9062) MACD2: 0.13961926871356733
購入: 9062 日通 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
丸井| 8252| -100| 1,425| -5
日通| 9062| 1000| 557| 1
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
日通| 9062| 1000| 557| 1
実行時間: 1.046 秒.
:
:
(略)
:
:
■28日目2005/04/28 木 終了時の所持金額
総資産 = 手持ち資金+所有株式時価総額 = 5,081,100円
手持ち資金(運転可能金額 + ロック金額) = 3,731,100円
運転可能金額 = 3,731,100円
ロック資金 = 0円
単純計算年率利益率 30.0%
○ロボットの銘柄評価
売却: 2914 JT 1単位 成行
注文銘柄: JT(2914) MACD2: 0.1513902822116494
購入: 2914 JT 1単位 成行
【注文処理結果】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
JT| 2914| -1| 1,360,000| -20,000
JT| 2914| 1| 1,360,000| -20,000
【ポートフォリオ】
銘柄名|銘柄コード| 枚数| 購入価格| 前日比
------------------------------+----------+----------+----------+----------
JT| 2914| 1| 1,360,000| -20,000
実行時間: 1.625 秒.
■29日目2005/04/29 金 終了時の所持金額
No Data
実行時間: 0.0 秒.
■30日目2005/04/30 土 終了時の所持金額
No Data
実行時間: 0.0 秒.
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
【最終結果】
あなたのロボットによる、該当期間の運用成績は、こちらです。
総資産 = 手持ち資金+所有株式時価総額 = 5,101,100円
単純計算年率利益率 36.0%
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
|
なんと、先程よりも更に利益が上がり、一ヶ月間で 101,100 円の利益になりました。もともとの MACD でもかなりいい利益が得られていたので、MACD 向きのシミュレート期間であることはなんとなく想像できるのですが、結果的には更に高い利益を得ることができました。
もちろん今回のこの改良が常に好成績につながるという保証はありませんが、世に多く出回っている各種のテクニカル指標を自分なりに改良してみて、更にその精度をシミュレートできる、というのはなかなか面白いと思いました。カブロボの自作ロボットではそんな実験もできるわけです。
リクエスト待ってます!
次回のテクニカル分析紹介ネタは未定です。読者の皆様からのフィードバックに間に合えば、次回の紹介ネタとさせていただきます。「このテクニカル分析をアルゴリズム化してほしい」という要望がありましたら、ぜひフィードバックとして送ってください。可能な限り挑戦します。
著者について  | |  | 木村 桂: 日本IBM ソフトウェア事業 |
記事の評価
|  |