IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Java technology  >

緊急特集!「スーパー・カブロボに挑戦」への第一歩: 後編

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

サンプルコード


レベル: 中級

木村 桂 (kimuc@jp.ibm.com), ソフトウェア事業, IBM 

2006年 6月 23日
2006年 8月 11日 更新

過去に日本アイ・ビー・エムおよび developerWorks を通じて運営・開催に協力してきたカブロボが、「スーパー」になって帰ってきました。カブロボコンテストは過去2回行われており、今回が第三回目になります。が、今回は趣向を大幅に変更し、改めて「第一回 スーパー・カブロボ・コンテスト」として開催されます。

少しだけ本格的(?)なロジックを実装

前回、スーパー・カブロボ・コンテストの記事を紹介したところ、予想以上に大きな反響があり、筆者自身も少し驚いています。本記事は前回に続いて Java によるロボットの開発方法を紹介する後編ですが、今回は少しだけ本格的な(変な日本語ですが・・)テクニカル分析を実装したロボット2体を紹介します。

紹介記事としては本記事が最終編ですが、このコーナーで取り上げてほしい! という要望がありましたら是非お聞かせください。著作権の問題や著者自身の経験不足から実現できないものもありますが、第2回コンテストの際に行ったような形で紹介させていただければ、と考えております。

なお、本記事にて紹介する投資方法が絶対的に正しいというわけではなく、技術としての紹介であることをご了承願います。投資判断は自己責任にてお願いいたします。




上に戻る


充実したテクニカル分析ライブラリ

特に前回大会までと比較して、スーパー・カブロボ・コンテスト用に提供された SDK を見ているとテクニカル分析用のライブラリが充実していることに気付きます。たとえば基本的な移動平均値を求めるための MovingAverage というクラスが用意されていて、このクラスを使って銘柄と日数を指定すれば、株価の単純移動平均値だけでなく、MACD の計算に用いる指数平滑平均値や加重移動平均値などを簡単に求めることができるようになっています。

 @Override
  public void order(TradeAgent arg0) {
    //. 「日足で20日間」の移動平均値のクラスを取得
    AnalysisManager analysisManager = arg0.getAnalysisManager();
    MovingAverage ma20 = analysisManager.getMovingAverage( EnumAnalysisSpan.DAILY, 20 );

    for( Stock stock : arg0.getInformationManager().getStockList() ){
      //. 各銘柄で本日時点での20日間の各種平均値を求める
      Double ma1v = ma20.getIndexSimple( stock );       //. 単純移動平均値
      Double ma2v = ma20.getIndexExponential( stock );  //. 指数平滑平均値
      Double ma3v = ma20.getIndexWeighted( stock );     //. 加重移動平均値

        :
        :
    }
  }

また移動平均値以外にも RSI や ボリンジャーバンド、MACD や一目均衡表などといった主要なテクニカル分析用のクラスが標準で用意されており、これらを使ったテクニカル分析が簡単にできるようになっています。

  @Override
  public void order(TradeAgent arg0) {
    AnalysisManager analysisManager = arg0.getAnalysisManager();

    //. 「日足で12日間」の RSI のクラスを取得
    RSI rsi12 = analysisManager.getRSI( EnumAnalysisSpan.DAILY, 12 );

    //. 「日足で20日間」のボリンジャーバンドのクラスを取得
    BolingerBand bb20 = analysisManager.getBolingerBand( EnumAnalysisSpan.DAILY, 20 );

    //. 「日足で短期=13、長期=22、シグナル=9」の MACD のクラスを取得
    MACD macd = analysisManager.getMACD( EnumAnalysisSpan.DAILY, 13, 22, 9 );

    for( Stock stock : arg0.getInformationManager().getStockList() ){
      //. 各銘柄で本日時点での RSI の指標値を求める
      Double rsiv = rsi12.getIndexSimple( stock );       //. RSI

      //. 各銘柄で本日時点でのボリンジャーバンドの指標値を求める
      Double bb1v = bb20.getIndexPlus2( stock );         //. +σ2
      Double bb2v = bb20.getIndexPlus1( stock );         //. +σ1
      Double bb3v = bb20.getIndexSMA( stock );           //. ±0
      Double bb4v = bb20.getIndexMinus1( stock );        //. -σ1
      Double bb5v = bb20.getIndexMinus2( stock );        //. -σ2

      //. 各銘柄で本日時点での MACD の指標値を求める
      Double macdv = macd.getIndexMACD( stock );         //. MACD
      Double sgnlv = macd.getIndexMACDAverage( stock );  //. シグナル

        :
        :
    }
  }

そこで、SDK で用意されているテクニカル分析用クラスの中から MACD のクラスを使った投資ロボットを作ってみることにします。またこの中で今回のルールに合わせた空売りの手法についても紹介します。




上に戻る


MACDドテン売買ロボットを作る

数あるテクニカル指標の中でも、MACD(Moving Average Convergence/Divergence)は「精度が高い」と耳にすることが多い気がしています。このテクニカル指標は移動平均の考え方をベースにしていますが、単純な移動平均値を求めるのではなく、最も直近のデータを最重要視する指数平滑平均値を求めている点が特徴の1つです。ごく簡単な内容ではありますが、過去のカブロボ大会向けに編集した記事もございますので、こちらも参照ください(注 参照記事内で紹介しているロボットのプログラミングコードはスーパー・カブロボ・コンテストとは互換性がありません。MACD の紹介とロジックの考え方のみ参考にしてください)。

この MACD は SIGNAL という指標と合わせてゴールデンクロス/デッドクロスという買い/売りのサインを見つけることができます。このサインに合わせて自動的に成行売買を行うようなロボットを作ってみることにします。なおデッドクロス時には空売り注文を発行することになりますが、スーパー・カブロボ・コンテストにおいては空売りにはアップティックルールが適用されますので、このルールにあわせた指値の空売り命令を発行します。更にこの指値空売り注文が1日以内に成立しなければキャンセルします。 また、今回のルールではロボットは前場前と後場前の1日に2回売買の機会がありますが、今回のロボットでは前場前のみに稼動するようなロジックにします。

以上のようなロボットを Java で作成すると以下のようになります。まずはソースコードをご覧ください。ここではロボットの名前は MyRobot002 としましたが、自由に名づけていただいて構いません。なおロボットのプログラミング手順については本記事の前編を参照ください:

import java.util.ArrayList;
import java.util.Calendar;

import jp.tradesc.superkaburobo.sdk.robot.AbstractRobot;
import jp.tradesc.superkaburobo.sdk.trade.AnalysisManager;
import jp.tradesc.superkaburobo.sdk.trade.EnumAnalysisSpan;
import jp.tradesc.superkaburobo.sdk.trade.InformationManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderHistoryManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderManager;
import jp.tradesc.superkaburobo.sdk.trade.PortfolioManager;
import jp.tradesc.superkaburobo.sdk.trade.TimeManager;
import jp.tradesc.superkaburobo.sdk.trade.TradeAgent;
import jp.tradesc.superkaburobo.sdk.trade.analysis.technicalindex.MACD;
import jp.tradesc.superkaburobo.sdk.trade.data.NotExecutedOrder;
import jp.tradesc.superkaburobo.sdk.trade.data.Portfolio;
import jp.tradesc.superkaburobo.sdk.trade.data.Stock;

/*
 * MACD と SIGNAL のゴールデンクロス/デッドクロスに合わせてドテン売買する
 * 
 * テクニック(1): MACD の指標数値を取得して、ゴールデンクロス/デッドクロスの判断をする
 * テクニック(2): 日付時刻の情報を取得・参照して、午前(あるいは午後)のみ売買するようにする
 * テクニック(3): デッドクロス時にアップティックルールに合わせて指値で空売りする
 * テクニック(4): 空売りの指値注文が約定したかどうかを翌日に調べ、約定していなければキャンセルする
 */
public class MyRobot002 extends AbstractRobot {
  //. MACD 計算用のパラメータ (1)
  static final int MA1 = 12; //. 短期EMAの計算日数
  static final int MA2 = 26; //. 長期EMAの計算日数
  static final int SGNL = 9; //. シグナルの計算日数

  //. デッドクロス時に空売り注文を発行するかどうか?(2)
  static final boolean sellFirst = true; //. true:する false:しない

  @Override
  public void order(TradeAgent arg0) {
    // TODO Auto-generated method stub
    //. 午前中のみの売買を行うため、市場の開始時刻を調べる(3)
    TimeManager timeManager = arg0.getTimeManager();
    Calendar c = timeManager.getCurrentDate();
    int hour = c.get( Calendar.HOUR );  //. 8:午前場, 11:午後場
    if( hour == 8 ){
      //. 注文執行用のオブジェクトを用意(4)
      OrderManager orderManager = arg0.getOrderManager();

      //. 空売りの注文が結果として約定したかどうかをチェックする(5)
      if( sellFirst ){
        OrderHistoryManager orderHistoryManager = arg0.getOrderHistoryManager();
        ArrayList<NotExecutedOrder> notExecutedOrderList =
            orderHistoryManager.getNotExecutedOrderList();
        for( NotExecutedOrder notExecutedOrder : notExecutedOrderList ){
          //. 約定しなかった前日の指値注文は全てキャンセルする(6)
          notExecutedOrder.orderCancel();
        }
      }
       
      //. 現在のポートフォリオを取得(7)
      PortfolioManager portfolioManager = arg0.getPortfolioManager();
      ArrayList<Portfolio> portfolioList = portfolioManager.getPortfolio();
       
      //. MACD の指標数値を調べるための準備(8)
      AnalysisManager analysisManager = arg0.getAnalysisManager();
      MACD macd = analysisManager.getMACD( EnumAnalysisSpan.DAILY, MA1, MA2, SGNL );
       
      //. 各銘柄ごとに買い/売りのサインを調べて、発注する(9)
      for( Stock stock : arg0.getInformationManager().getStockList() ){
        //. 例外処理発生時の対応(10)
        try{
          //. 本日の MACD に関係する指標値を取得(11)
          Double macdt = macd.getIndexMACD( stock );  //. MACD
          Double sgnlt = macd.getIndexMACDAverage( stock ); //. SIGNAL
          Double macd2t = macdt - sgnlt;    //. MACD2(=MACD-SIGNAL)
          
          //. 前営業日の MACD に関係する指標値を取得(12)
          Double macdy = macd.getIndexMACD( stock, 1 );  //. MACD
          Double sgnly = macd.getIndexMACDAverage( stock, 1 ); //. SIGNAL
          Double macd2y = macdy - sgnly;    //. MACD2(=MACD-SIGNAL)

          //. MACD のグラフと SIGNAL のグラフがクロスしたかどうかを調べるには、
          //. MACD2(=MACD-SIGNAL) の値がプラスからマイナスへ、
          //. 或いはマイナスからプラスへ転じたかどうかを調べればよい。
          //. そこで前営業日の MACD2 の値と本日の MACD2 の値を掛け合わせて、
          //. 値が負になっているかどうかを調べることにする。(13)
          if( macd2y * macd2t < 0 ){
            //. MACD と SIGNAL がクロスした!
           
            //. 売買にそなえてこの銘柄の単元株数を取得しておく(14)
            Integer unit = stock.getUnit(); //. 単元数
           
            //. 空売りをしない場合は、注文株式数を倍にする(15)
            if( !sellFirst ){
              unit = 2 * unit;
            }
           
            //. クロスした結果、MACD が SIGNAL の上に来ていればゴールデンクロス、
            //. MACD が SIGNAL の下に来ていればデッドクロスである。(16)
            if( macdt > sgnlt ){
              //. ゴールデンクロス(=買いのサイン)
            
              //. 現在、空売りポジションを持っているか?(17)
              for( Portfolio portfolio : portfolioList ){
                if( sellFirst & portfolio.getStock().equals( stock ) ){
                  //. 空売りしていた場合は決済する(18)
                  portfolio.orderReverseNowMarketAll();
                }
              }
            
              //. 新規の買い注文 (19)
              orderManager.orderActualNowMarket( stock, unit );
            }else{
              //. デッドクロス(=売りのサイン)

              //. 現在、買いポジションを持っているか?(20)
              for( Portfolio portfolio : portfolioList ){
                if( portfolio.getStock().equals( stock ) ){
                  //. 買っていた場合は決済する(21)
                  portfolio.orderReverseNowMarketAll();
                }
              }
            
              //. 空売りアリの設定になっていたら、新規の空売りを執行する(22)
              if( sellFirst  ){
                //. 空売りアリの設定になっていた
            
                //. 空売りの場合、アップティックルールにより
                //. 直近値より低い値段では売れず、また指値のみ(23)
                InformationManager informationManager = arg0.getInformationManager();
                Integer closingPrice =
                    informationManager.getStockSession( stock ).getClosingPrice();
                Integer sp = informationManager.checkSpread( closingPrice ); //. 呼び値

                //. アップティックルールに基づき、終値よりも呼び値だけ上乗せして指値注文を執行する(24)
                orderManager.orderCreditNowLimit( stock, closingPrice + sp, -1 * unit );
              }
            }
          }
        }catch( Exception e ){
        }
      }
    }
  }

  @Override
  public void screening(TradeAgent arg0) {
    // TODO Auto-generated method stub

  }

}

内容を簡単に紹介します。プログラムソース中のコメント緑字部分と合わせて確認してください。

まずはプログラム内で用いる変数の定義を行います。最初にMACD を計算するためのパラメータを定義してします(1)。具体的には短期EMA(指数平滑平均値)と長期EMAを計算する日数と、SIGNAL を計算する日数の3つです。次に MACD がデッドクロスした場合に空売りを実施するかどうかを定義しています(2)。この値が true であれば空売りを執行しますが、false の場合は空売りを行わないロボット、ということになります。このロボットはスクリーニングを行わないため、全てのロジックは全て売買処理(order関数)内に記述されています。

order 関数内ではまず TimeManager クラスを利用して、市場の開始時刻を調べます(3)。これによって午前場か午後場かを判断できるので、今回は午前場のみの発注を行うようにしました。午前場であることが確認できた後は注文処理発行に備えて OrderManager のオブジェクトを作成(4)した後で、前日に空売りの指値注文をしていた場合はそれが成立したかどうかを調べます(5)。ここでは OrderHistoryManager クラスから NotExecutedOrder オブジェクトのリストを取得します。指値注文が成立していない場合はこの中に各注文が入っているので、それぞれ個別にキャンセルします(6)

この後 MACD と SIGNAL とを計算して売買タイミングを測るのですが、その際に同じ銘柄で既にポジションを持っている場合は決済してからドテン売買することになります。そのため現在のポジション情報であるポートフォリオを取得します(7)。また MACD を計算するための準備として AnalysisManager から (1) で指定したパラメータでの MACD 計算用オブジェクトを取得しておきます(8)

ここからが各銘柄ごとに MACD のゴールデンクロス/デッドクロスを検知するロジックになります(9)。まずは本日時点での MACD と SIGNAL の数値を計算し、これら2つの値の差である MACD2 を導きます(11)。同様にして前営業日の MACD2 を求めます(12)。MACD と SIGNAL がクロスするというのは MACD と SIGNAL の差である MACD2 の符号がプラスからマイナスへ、或いはマイナスからプラスへ変わることを意味しています。そのため前日と本日の MACD2 の値を掛け合わせて、結果がマイナスになった場合はゴールデンクロスかデッドクロスが発生した、と判断できることになります(13)。 その場合はこの銘柄の売買に備えて株式コードと単元株数を取得しておきます(14)。またここは必須ではありませんが、(2)で空売りをしない設定(新規注文は「買い」のみの設定)にした場合は、コンテスト審査基準の1つでもある売買取引実績を調整する目的で空売りをする設定の場合と比べて倍の株数を注文するようにしています(15)

次に改めて本日の MACD 値と SIGNAL 値とを比較し、ゴールデンクロスが発生していたのか、デッドクロスが発生していたのかを切り分けます(16)。上昇のサインであるゴールデンクロスが発生していた場合、まず同じ銘柄の株を空売りしているかどうかを調べ(17)、していた場合は決済します(18)。この処理の後で改めて購入の注文を発行します(19)。 一方、下落のサインであるデッドクロスが発生していた場合も、まずは同じ銘柄の株を購入しているかどうかを調べて(20)、していた場合は決済します(21)。その後、同銘柄を空売りすることになります(22)が、スーパー・カブロボ・コンテストでは実資運用時に備えて、機関投資家に適用されることの多いアップティックルールが採用されています。具体的には空売り注文時には「指値注文のみ」で「前日終値以下の空売り注文ができない」というルールが適用されますので、このロボットでは「前日終値プラス呼び値」を求めて(23)、その指値で空売りすることにします(24)。なお、この指値注文が成立しなかった場合は (5)(6) の処理でキャンセルされることになります。これを全ての銘柄(セルフテスト時には20銘柄、予備審査時には30銘柄、本運用では500銘柄)に対して繰り返し行います。

また、説明の順序が遅れましたが、一時的な取引停止など、何らかの原因で個別銘柄の指標値が取得できなかった場合に備えて、Java の例外処理を行っています(10)。これによってある銘柄で値が取得できずにエラーが発生しても、そこでその日の処理を止めずに、次の銘柄の指標値を調査するようになります。

以上がこの MyRobot002 の動作ロジックです。プログラムのコメント内にも記述していますが、

  • テクニックその1: MACD のテクニカル指標を元にゴールデンクロス/デッドクロスにドテン売買する
  • テクニックその2: 午前場のみ売買する
  • テクニックその3: デッドクロス時の空売りをスーパー・カブロボ・コンテストのルールに従って執行する
  • テクニックその4: 指値注文が1日で成立しなければキャンセルする

という4つのテクニックをロジックで実現しています。皆さんが独自のロボットを作成する際に役立つことがあれば是非参考にしてください。





上に戻る


MACDドテン売買ロボットを動かす

では作成したこのロボットを動かしてみます。前編記事でも紹介しましたが、パッケージエクスプローラなどから robot-config.xml を選択して以下の内容で保存します。重要なのは赤字の部分で、MyRobot002 の部分には作成したロボットの名前 を入力します。またテストの期間を2004年9月1日から2005年8月31日までに戻しておき、セルフテストの基準を満たしているかどうかを測ってみます。

<?xml version="1.0" encoding="UTF-8" ?>
<robot-config>
  <robot-class-name>MyRobot002</robot-class-name>
  <time>
    <start>2004-09-01</start>
    <end>2005-08-31</end>
  </time>
  <user-log>
    <console level="5" />
    <file level="0" path="log.txt" />
    <database level="0" />
  </user-log>
  <system-log>true</system-log>
  <config-confirmation>false</config-confirmation>
  <overwrite>true</overwrite>
</robot-config>
            

この状態で MyRobot002 を実行すると以下のような結果になります。実際には1年分の売買履歴が延々と含まれていますが、ここでは最終成績の部分のみを記載しています:

■■最終成績表■■■■■■■■■■■■■■■■■■■■
--●取引データ●--------------------------------------
取引開始日             :2004-09-01
取引終了日             :2005-08-31
経過日数(日)           :365
運用日数(日)           :245
総トレード数            :350
勝ちトレード数           :127
負けトレード数           :223
年間平均トレード数         :249
連続勝ちトレード数         :5
連続負けトレード数         :12
全トレード平均期間(日)      :17
勝ちトレード平均期間(日)     :26
負けトレード平均期間(日)     :12
最長フラット期間(日)       :1
トータル約定金額(円)       :317779275
--●勝率データ●--------------------------------------
勝率(%)             :36.29
最大年間勝ち月数率(%)      :66.67
最小年間勝ち月数率(%)      :66.67
平均年間勝ち月数率(%)      :50
--●損益データ●--------------------------------------
トータル純損益(%)        :-0.19
勝ちトレード純利益(%)      :4.68
負けトレード純損失(%)      :-4.77
買いトレード純損益(%)      :0.85
売りトレード純損益(%)      :-0.94
平均損益(%)           :-0.22
平均利益(%)           :3.77
平均損失(%)           :-2.49
年次平均損益(%)         :-0.2
最大勝ちトレード(%)       :36.54
最大負けトレード(%)       :-12.14
--●指標データ●--------------------------------------
平均ドローダウン(%)       :0.81
最大ドローダウン(%)       :1.43
損益レシオ(倍)          :1.72
プロフィットファクター(倍)    :0.98
年次平均損益/最大ドローダウン(倍):-0.14
月次シャープレシオ(倍)      :-0.04
年次シャープレシオ(倍)      :-0.13
新規シグナル発生確率(%)     :5.05
返済シグナル発生確率(%)     :32.11
年次利回りボラティリティ(%)   :1.49
--●エントリー基準●----------------------------------
トータル約定金額 [1億円以上]   :合格
トータル純損益  [ 0%以上]   :不合格
最大ドローダウン [30%未満]   :合格
■■■■■■■■■■■■■■■■■■■■■■■■■■■
ロボット注文処理時間合計(ミリ秒): 182624
ロボット注文処理時間平均(ミリ秒): 375
ロボットスクリーニング処理時間合計(ミリ秒): 9983
ロボットスクリーニング処理時間平均(ミリ秒): 40
総実行時間(ミリ秒): 287438

赤字のエントリー基準という点では、約定金額とドローダウン率は合格でしたが、残念ながら(おそらく一番肝心な)利益をあげることができなかったため残念ながら不合格でした。エントリーはこの3つ全て合格していないと認められません。また紫字で示した勝率も 36.29% とあまり褒められたものではありません。勝率ロジックそのものを見直すか、あるいはプログラムコードの (1)(2) のパラメータを調整してみる必要がありそうです。

※なお (2) の変数値を sellFirst = false; と変更し、空売りをしない設定で再度シミュレート実行したところ、トータル純損益が 1.91% となり、トータル約定金額や最大ドローダウン率と合わせてすべて合格になりました。勝率も 42.31% とかなりよくなっています。空売りをしない設定であれば、一応参加基準を満たしていることになります。

ところで、エントリー基準とは別に気にする必要があるのが青字で示したロボットの処理時間です。現時点でのルールでは大会本番では 500 銘柄を対象に実行する条件下で注文処理を3分以内、スクリーニング処理を8分以内に行う必要があります(要望によっては見直しがあるそうです)。今回のシミュレートは 20 銘柄で行っていますから、仮に処理時間が比例関係で増加すると仮定した場合でも、本番では 25 倍の処理時間がかかることになります。また注文処理について、この MyRobot002 は事実上午前場のみ稼動している(つまり母数の半分しか実際の処理を行っていない)ことから、午前場だけの注文処理時間平均という意味では、ここに記録された数値の2倍の約 0.7 秒かかっているとみるべきだと思います。ということは 500 銘柄ではこの 25 倍の 17.5 秒が1回の注文に必要、ということになります。

本番での運用サーバーと筆者の開発用ノート PC とを単純比較することはできませんが、とりあえずここの多少の処理を追加することがあってもルール上の3分以内に収まるのでは? と推測できます。また今回のロボットでは事実上スクリーニング処理を行っていないので関係ないのですが、スクリーニング処理は1回につき8分以内で行う必要があります。皆さんが独自にロボットを作成する際は、これらの点にも注意してください。





上に戻る


高速化を求めて改良

上記の MyRobot002 のロジックを理解することで、他のアルゴリズムやテクニカル指標を利用する場合に応用できると考えています。本記事の最後にその一例を紹介します。過去のカブロボ大会向けに編集した記事でも紹介しましたが、MACD の計算途中で EMA(指数平滑平均)を求めるのではなく、WMA(加重平均)を求めるように改良して、より最近のデータをより重要視するような計算方法で MACD を求めるように改良します。

また、この MyRobot002 の実行結果の一番最後を見ると分かるのですが、筆者のノート PC では1回のシミュレーションを行う際に約 20 分かかっています。あまり気軽にテストできるものではありません。作成したロボットの動作確認だけであればシミュレーションの期間をもっと短期間に変更するなどすればよいのですが、プログラムロジックを更に複雑にした場合などは思考時間が制限時間を超えてしまう恐れもあります。その点もカブロボ SDK の汎用ライブラリを使わないことで、ロボットの高速化に挑戦します。

改良したロボットの名前を MyRobot003 としました。プログラムコードは以下のようになります。MyRobot002 からの変更箇所は青字で示しました:

import java.util.ArrayList;
import java.util.Calendar;

import jp.tradesc.superkaburobo.sdk.robot.AbstractRobot;
import jp.tradesc.superkaburobo.sdk.trade.AnalysisManager;
import jp.tradesc.superkaburobo.sdk.trade.InformationManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderHistoryManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderManager;
import jp.tradesc.superkaburobo.sdk.trade.PortfolioManager;
import jp.tradesc.superkaburobo.sdk.trade.TimeManager;
import jp.tradesc.superkaburobo.sdk.trade.TradeAgent;
import jp.tradesc.superkaburobo.sdk.trade.analysis.technicalindex.MovingAverage;
import jp.tradesc.superkaburobo.sdk.trade.data.NotExecutedOrder;
import jp.tradesc.superkaburobo.sdk.trade.data.Portfolio;
import jp.tradesc.superkaburobo.sdk.trade.data.Stock;
import jp.tradesc.superkaburobo.sdk.trade.data.StockData;

/*
 * MACD を独自に改良して、ゴールデンクロス/デッドクロスに合わせてドテン売買する
 * 
 * テクニック(1'): MACD の指標数値をそのまま使わず、独自に計算して、ゴールデンクロス/デッドクロスの判断をする
 * テクニック(2) : 日付時刻の情報を取得・参照して、午前(あるいは午後)のみ売買するようにする
 * テクニック(3) : デッドクロス時にアップティックルールに合わせて指値で空売りする
 * テクニック(4) : 空売りの指値注文が約定したかどうかを翌日に調べ、約定していなければキャンセルする
 */
public class MyRobot003 extends AbstractRobot {
  //. MACD 計算用のパラメータ
  static final int MA1 = 12; //. 短期WMAの計算日数
  static final int MA2 = 26; //. 長期WMAの計算日数
  static final int SGNL = 9; //. シグナルの計算日数

  //. デッドクロス時に空売り注文を発行するかどうか?
  static final boolean sellFirst = true; //. true:する false:しない

  @Override
  public void order(TradeAgent arg0) {
    // TODO Auto-generated method stub
    //. 午前中のみの売買を行うため、市場の開始時刻を調べる
    TimeManager timeManager = arg0.getTimeManager();
    Calendar c = timeManager.getCurrentDate();
    int hour = c.get( Calendar.HOUR );  //. 8:午前場, 11:午後場
    if( hour == 8 ){
      //. 注文執行用のオブジェクトを用意
      OrderManager orderManager = arg0.getOrderManager();

      //. 空売りの注文が結果として約定したかどうかをチェックする
      if( sellFirst ){
        OrderHistoryManager orderHistoryManager = arg0.getOrderHistoryManager();
        ArrayList<NotExecutedOrder> notExecutedOrderList =
            orderHistoryManager.getNotExecutedOrderList();
        for( NotExecutedOrder notExecutedOrder : notExecutedOrderList ){
          //. 約定しなかった前日の指値注文は全てキャンセルする 
          notExecutedOrder.orderCancel();
        }
      }
       
      //. 現在のポートフォリオを取得
      PortfolioManager portfolioManager = arg0.getPortfolioManager();
      ArrayList<Portfolio> portfolioList = portfolioManager.getPortfolio();
       
      //. MACD の指標数値を改良して調べるため、移動平均オブジェクトの準備
      AnalysisManager analysisManager = arg0.getAnalysisManager();
      MovingAverage ma = analysisManager.getMovingAverage();
                //. MACD を独自の方法で計算するために必要な
      //. データの取得期間(営業日で ( MA2 + SGNL ) 日間)を決定する
                Calendar c1 = timeManager.getBusinessDay( -1 * ( MA2 + SGNL ) );
      Calendar c2 = timeManager.getCurrentDate();
         
      //. 各銘柄ごとに買い/売りのサインを調べて、発注する
      for( Stock stock : arg0.getInformationManager().getStockList() ){
        //. 例外処理発生時の対応
        try{
          int cp[] = new int[MA2 + SGNL];
                //. 指定期間中の株価終値を1日ごとの配列で取り出す
          int i = 0;
          ArrayList<StockData> stockDataList 
              = arg0.getInformationManager().getStockDailyByInterval( c1, c2, stock );
          for( StockData stockData : stockDataList ){
            cp[i++] = stockData.getClosingPrice();  //. 終値
          }
                //. 終値から本日の移動平均値と MACD を計算する
          double wma1t[] = new double[SGNL];
          double wma2t[] = new double[SGNL];
          double macdt[] = new double[SGNL];
          for( i = 0; i < SGNL; i ++ ){
            int p[] = new int[MA2];
            for( int j = 0; j < MA2; j ++ ){
              p[j] = cp[i+j];
            }
            wma1t[i] = ma.getBaseIndexWeighted( p, MA1 ); //. EMAの代わりにWMAを使う
            wma2t[i] = ma.getBaseIndexWeighted( p, MA2 );
            macdt[i] = wma1t[i] - wma2t[i];
          }
                //. 本日の MACD 関連指標値を求める
          Double wma1 = wma1t[0];      //. 本日の短期WMA
          Double wma2 = wma2t[0];      //. 本日の長期WMA
          Double macdtv = wma1 - wma2;     //. 本日のMACD
          Double sgnltv = ma.getBaseIndexSimple( macdt, SGNL ); //. 本日のSIGNAL
          Double macd2t = macdtv - sgnltv;     //. 本日のMACD2(=MACD-SIGNAL)
                //. 同様にして前営業日の MACD 関連指標値を求める
          double wma1y[] = new double[SGNL];
          double wma2y[] = new double[SGNL];
          double macdy[] = new double[SGNL];
          for( i = 0; i < SGNL; i ++ ){
            int p[] = new int[MA2];
            for( int j = 0; j < MA2; j ++ ){
              p[j] = cp[1+i+j];
            }
            wma1y[i] = ma.getBaseIndexWeighted( p, MA1 ); //. EMAの代わりにWMAを使う
            wma2y[i] = ma.getBaseIndexWeighted( p, MA2 );
            macdy[i] = wma1y[i] - wma2y[i];
          }
                wma1 = wma1y[0];
          wma2 = wma2y[0];
          Double macdyv = wma1 - wma2;
          Double sgnlyv = ma.getBaseIndexSimple( macdy, SGNL );
          Double macd2y = macdyv - sgnlyv;     //. 前営業日のMACD2(=MACD-SIGNAL)
          
          //. MACD のグラフと SIGNAL のグラフがクロスしたかどうかを調べるには、
          //. MACD2(=MACD-SIGNAL) の値がプラスからマイナスへ、
          //. 或いはマイナスからプラスへ転じたかどうかを調べればよい。
          //. そこで前営業日の MACD2 の値と本日の MACD2 の値を掛け合わせて、
          //. 値が負になっているかどうかを調べることにする。 
          if( macd2y * macd2t < 0 ){
            //. MACD と SIGNAL がクロスした!
           
            //. 売買にそなえてこの銘柄の単元株数を取得しておく
            Integer unit = stock.getUnit(); //. 単元数
           
            //. 空売りをしない場合は、注文株式数を倍にする
            if( !sellFirst ){
              unit = 2 * unit;
            }
           
            //. クロスした結果、MACD が SIGNAL の上に来ていればゴールデンクロス、
            //. MACD が SIGNAL の下に来ていればデッドクロスである。
            if( macdtv > sgnltv ){
              //. ゴールデンクロス(=買いのサイン)
            
              //. 現在、空売りポジションを持っているか?
              for( Portfolio portfolio : portfolioList ){
                if( sellFirst & portfolio.getStock().equals( stock ) ){
                  //. 空売りしていた場合は決済する
                  portfolio.orderReverseNowMarketAll();
                }
              }
            
              //. 新規の買い注文
              orderManager.orderActualNowMarket( stock, unit );
            }else{
              //. デッドクロス(=売りのサイン)

              //. 現在、買いポジションを持っているか? 
              for( Portfolio portfolio : portfolioList ){
                if( portfolio.getStock().equals( stock ) ){
                  //. 買っていた場合は決済する (21)
                  portfolio.orderReverseNowMarketAll();
                }
              }
            
              //. 空売りアリの設定になっていたら、新規の空売りを執行する
              if( sellFirst  ){
                //. 空売りアリの設定になっていた
            
                //. 空売りの場合、アップティックルールにより
                //. 直近値より低い値段では売れず、また指値のみ
                InformationManager informationManager = arg0.getInformationManager();
                Integer closingPrice =
                    informationManager.getStockSession( stock ).getClosingPrice();
                Integer sp = informationManager.checkSpread( closingPrice ); //. 呼び値

                //. アップティックルールに基づき、終値よりも呼び値だけ上乗せして指値注文を執行する
                orderManager.orderCreditNowLimit( stock, closingPrice + sp, -1 * unit );
              }
            }
          }
        }catch( Exception e ){
        }
      }
    }
  }

  @Override
  public void screening(TradeAgent arg0) {
    // TODO Auto-generated method stub

  }

}


import java.util.ArrayList;
import java.util.Calendar;

import jp.tradesc.superkaburobo.sdk.robot.AbstractRobot;
import jp.tradesc.superkaburobo.sdk.trade.AnalysisManager;
import jp.tradesc.superkaburobo.sdk.trade.InformationManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderHistoryManager;
import jp.tradesc.superkaburobo.sdk.trade.OrderManager;
import jp.tradesc.superkaburobo.sdk.trade.PortfolioManager;
import jp.tradesc.superkaburobo.sdk.trade.TimeManager;
import jp.tradesc.superkaburobo.sdk.trade.TradeAgent;
import jp.tradesc.superkaburobo.sdk.trade.analysis.technicalindex.MovingAverage;
import jp.tradesc.superkaburobo.sdk.trade.data.NotExecutedOrder;
import jp.tradesc.superkaburobo.sdk.trade.data.Portfolio;
import jp.tradesc.superkaburobo.sdk.trade.data.Stock;
import jp.tradesc.superkaburobo.sdk.trade.data.StockData;

/*
 * MACD を独自に改良して、ゴールデンクロス/デッドクロスに合わせてドテン売買する
 * 
 * テクニック(1'): MACD の指標数値をそのまま使わず、独自に計算して、ゴールデンクロス/デッドクロスの判断をする
 * テクニック(2) : 日付時刻の情報を取得・参照して、午前(あるいは午後)のみ売買するようにする
 * テクニック(3) : デッドクロス時にアップティックルールに合わせて指値で空売りする
 * テクニック(4) : 空売りの指値注文が約定したかどうかを翌日に調べ、約定していなければキャンセルする
 */
public class MyRobot003 extends AbstractRobot {
  //. MACD 計算用のパラメータ
  static final int MA1 = 12; //. 短期WMAの計算日数
  static final int MA2 = 26; //. 長期WMAの計算日数
  static final int SGNL = 9; //. シグナルの計算日数

  //. デッドクロス時に空売り注文を発行するかどうか?
  static final boolean sellFirst = true; //. true:する false:しない

  @Override
  public void order(TradeAgent arg0) {
    // TODO Auto-generated method stub
    //. 午前中のみの売買を行うため、市場の開始時刻を調べる
    TimeManager timeManager = arg0.getTimeManager();
    Calendar c = timeManager.getCurrentDate();
    int hour = c.get( Calendar.HOUR );  //. 8:午前場, 11:午後場
    if( hour == 8 ){
      //. 注文執行用のオブジェクトを用意
      OrderManager orderManager = arg0.getOrderManager();

      //. 空売りの注文が結果として約定したかどうかをチェックする
      if( sellFirst ){
        OrderHistoryManager orderHistoryManager = arg0.getOrderHistoryManager();
        ArrayList<NotExecutedOrder> notExecutedOrderList =
            orderHistoryManager.getNotExecutedOrderList();
        for( NotExecutedOrder notExecutedOrder : notExecutedOrderList ){
          //. 約定しなかった前日の指値注文は全てキャンセルする 
          notExecutedOrder.orderCancel();
        }
      }
       
      //. 現在のポートフォリオを取得
      PortfolioManager portfolioManager = arg0.getPortfolioManager();
      ArrayList<Portfolio> portfolioList = portfolioManager.getPortfolio();
       
      //. MACD の指標数値を改良して調べるため、移動平均オブジェクトの準備
      AnalysisManager analysisManager = arg0.getAnalysisManager();
      MovingAverage ma = analysisManager.getMovingAverage();
                //. MACD を独自の方法で計算するために必要な
      //. データの取得期間(営業日で ( MA2 + SGNL ) 日間)を決定する(25)
      Calendar c1 = timeManager.getBusinessDay( -1 * ( MA2 + SGNL ) );
      Calendar c2 = timeManager.getCurrentDate();
         
      //. 各銘柄ごとに買い/売りのサインを調べて、発注する
      for( Stock stock : arg0.getInformationManager().getStockList() ){
        int cp[] = new int[MA2 + SGNL];
          
        //. 指定期間中の株価終値を1日ごとの配列で取り出す(26)
        int i = 0;
        ArrayList<StockData> stockDataList 
            = arg0.getInformationManager().getStockDailyByInterval( c1, c2, stock );
        for( StockData stockData : stockDataList ){
          cp[i++] = stockData.getClosingPrice();  //. 終値
        }
          
        //. 終値から本日の移動平均値と MACD を計算する(27)
        double wma1t[] = new double[SGNL];
        double wma2t[] = new double[SGNL];
        double macdt[] = new double[SGNL];
        for( i = 0; i < SGNL; i ++ ){
          int p[] = new int[MA2];
          for( int j = 0; j < MA2; j ++ ){
            p[j] = cp[i+j];
          }
          wma1t[i] = ma.getBaseIndexWeighted( p, MA1 ); //. EMAの代わりにWMAを使う
          wma2t[i] = ma.getBaseIndexWeighted( p, MA2 );
          macdt[i] = wma1t[i] - wma2t[i];
        }
          
        //. 本日の MACD 関連指標値を求める(28)
        Double wma1 = wma1t[0];      //. 本日の短期WMA
        Double wma2 = wma2t[0];      //. 本日の長期WMA
        Double macdtv = wma1 - wma2;     //. 本日のMACD
        Double sgnltv = ma.getBaseIndexSimple( macdt, SGNL ); //. 本日のSIGNAL
        Double macd2t = macdtv - sgnltv;     //. 本日のMACD2(=MACD-SIGNAL)

        //. 同様にして前営業日の MACD 関連指標値を求める(29)
        double wma1y[] = new double[SGNL];
        double wma2y[] = new double[SGNL];
        double macdy[] = new double[SGNL];
        for( i = 0; i < SGNL; i ++ ){
          int p[] = new int[MA2];
          for( int j = 0; j < MA2; j ++ ){
            p[j] = cp[1+i+j];
          }
          wma1y[i] = ma.getBaseIndexWeighted( p, MA1 ); //. EMAの代わりにWMAを使う
          wma2y[i] = ma.getBaseIndexWeighted( p, MA2 );
          macdy[i] = wma1y[i] - wma2y[i];
        }

        wma1 = wma1y[0];
        wma2 = wma2y[0];
        Double macdyv = wma1 - wma2;
        Double sgnlyv = ma.getBaseIndexSimple( macdy, SGNL );
        Double macd2y = macdyv - sgnlyv;     //. 前営業日のMACD2(=MACD-SIGNAL)
        
        //. MACD のグラフと SIGNAL のグラフがクロスしたかどうかを調べるには、
        //. MACD2(=MACD-SIGNAL) の値がプラスからマイナスへ、
        //. 或いはマイナスからプラスへ転じたかどうかを調べればよい。
        //. そこで前営業日の MACD2 の値と本日の MACD2 の値を掛け合わせて、
        //. 値が負になっているかどうかを調べることにする。 
        if( macd2y * macd2t < 0 ){
          //. MACD と SIGNAL がクロスした!
         
          //. 売買にそなえてこの銘柄の単元株数を取得しておく
          Integer unit = stock.getUnit(); //. 単元数
         
          //. 空売りをしない場合は、注文株式数を倍にする
          if( !sellFirst ){
            unit = 2 * unit;
          }
         
          //. クロスした結果、MACD が SIGNAL の上に来ていればゴールデンクロス、
          //. MACD が SIGNAL の下に来ていればデッドクロスである。
          if( macdtv > sgnltv ){
            //. ゴールデンクロス(=買いのサイン)
          
            //. 現在、空売りポジションを持っているか?
            for( Portfolio portfolio : portfolioList ){
              if( sellFirst & portfolio.getStock().equals( stock ) ){
                //. 空売りしていた場合は決済する
                portfolio.orderReverseNowMarketAll();
              }
            }
          
            //. 新規の買い注文
            orderManager.orderActualNowMarket( stock, unit );
          }else{
            //. デッドクロス(=売りのサイン)

            //. 現在、買いポジションを持っているか? 
            for( Portfolio portfolio : portfolioList ){
              if( portfolio.getStock().equals( stock ) ){
                //. 買っていた場合は決済する (21)
                portfolio.orderReverseNowMarketAll();
              }
            }
          
            //. 空売りアリの設定になっていたら、新規の空売りを執行する
            if( sellFirst  ){
              //. 空売りアリの設定になっていた
          
              //. 空売りの場合、アップティックルールにより
              //. 直近値より低い値段では売れず、また指値のみ
              InformationManager informationManager = arg0.getInformationManager();
              Integer closingPrice =
                  informationManager.getStockSession( stock ).getClosingPrice();
              Integer sp = informationManager.checkSpread( closingPrice ); //. 呼び値

              //. アップティックルールに基づき、終値よりも呼び値だけ上乗せして指値注文を執行する
              orderManager.orderCreditNowLimit( stock, closingPrice + sp, -1 * unit );
            }
          }
        }
      }
    }
  }

  @Override
  public void screening(TradeAgent arg0) {
    // TODO Auto-generated method stub

  }

}

基本的な部分はほぼ同じです。大きく異なるのは MACD 関連の数値を計算する部分ですが、処理負荷の大きそうな MACD のオブジェクトを利用せず、すべて定義通りに自分で計算しなおすようにした点と、EMA の代わりに WMA を使って MACD の値を求めている点が異なっています。本日の WMA や MACD, SIGNAL を独自に計算するために必要な相当日数分の期間開始日を決定(25)し、各銘柄ごとにその期間中の株価データから終値を取り出して配列変数に格納します(26)。ちなみに本日の MACD や SIGNAL を計算するために必要なデータ数は ( MA2 + SGNL - 1 ) ですが、今回は前営業日の値についても調べるため、1日余分にデータが必要になります。結果として ( MA2 + SGNL ) 日分のデータを取得することになります。

この後は移動平均のオブジェクトを用いて取り出した終値の配列から WMA と MACD を計算して(27)、最後に MACD の平均値を求めることで本日の SIGNAL と、MACD2 を計算します(28)。また同じロジックを1日ずらした終値を用いて計算することで前営業日の MACD や SIGNAL、MACD2 を計算します(29)。これらの値が取得できてしまえば後は MyRobot002 と同じです。

余談ですが、この (25)(26) の方法を利用することで、個別銘柄の過去の株価データを取得することができるようになります。developerWorks の過去のカブロボ大会向け記事で紹介している手法を試してみたい方は、この方法を応用して過去のデータを取得し、同様のロジックを実現させることに挑戦してみてください。

この改良した MyRobot003 を1年間シミュレートすると、以下のような最終結果が得られます(空売りアリの設定です):

■■最終成績表■■■■■■■■■■■■■■■■■■■■
--●取引データ●--------------------------------------
取引開始日             :2004-09-01
取引終了日             :2005-08-31
経過日数(日)           :365
運用日数(日)           :245
総トレード数            :323
勝ちトレード数           :132
負けトレード数           :191
年間平均トレード数         :228
連続勝ちトレード数         :9
連続負けトレード数         :12
全トレード平均期間(日)      :18
勝ちトレード平均期間(日)     :25
負けトレード平均期間(日)     :14
最長フラット期間(日)       :1
トータル約定金額(円)       :283824000
--●勝率データ●--------------------------------------
勝率(%)             :40.87
最大年間勝ち月数率(%)      :66.67
最小年間勝ち月数率(%)      :66.67
平均年間勝ち月数率(%)      :58.33
--●損益データ●--------------------------------------
トータル純損益(%)        :0.45
勝ちトレード純利益(%)      :5.14
負けトレード純損失(%)      :-4.18
買いトレード純損益(%)      :1.72
売りトレード純損益(%)      :-0.75
平均損益(%)           :-0.07
平均利益(%)           :4.13
平均損失(%)           :-2.98
年次平均損益(%)         :0.49
最大勝ちトレード(%)       :31.15
最大負けトレード(%)       :-14.02
--●指標データ●--------------------------------------
平均ドローダウン(%)       :0.69
最大ドローダウン(%)       :1.35
損益レシオ(倍)          :1.78
プロフィットファクター(倍)    :1.23
年次平均損益/最大ドローダウン(倍):0.37
月次シャープレシオ(倍)      :0.09
年次シャープレシオ(倍)      :0.33
新規シグナル発生確率(%)     :4.64
返済シグナル発生確率(%)     :25.21
年次利回りボラティリティ(%)   :1.49
--●エントリー基準●----------------------------------
トータル約定金額 [1億円以上]   :合格
トータル純損益  [ 0%以上]   :合格
最大ドローダウン [30%未満]   :合格
■■■■■■■■■■■■■■■■■■■■■■■■■■■
ロボット注文処理時間合計(ミリ秒): 122953
ロボット注文処理時間平均(ミリ秒): 252
ロボットスクリーニング処理時間合計(ミリ秒): 12071
ロボットスクリーニング処理時間平均(ミリ秒): 49
総実行時間(ミリ秒): 277812
            

純損益がプラスになっただけでなく、最大ドローダウンも小さくなりました。勝率もわずかながら改善され、結果的に3つのエントリー基準を全て満たすことに成功しています。ちなみにこのロボットを空売りナシの設定でシミュレートすると純損益は 3.03% になります。この期間は株価が上昇基調にあるせいか、空売りロジックには不利な期間なのかもしれません。

更に注目すべきは青字の処理時間です。MACD をより複雑な方法で計算したにも関わらず、総実行時間を短縮することができました。 負荷の大きなテクニカル指標は自分で計算しなおすのも悪くないと思いますし、なによりも従来のテクニカル指標にとらわれないアルゴリズムを探し出すのが、このコンテストの目的の1つでもあるので、もしかすると実資運用ロボットに選ばれる可能性が高くなるかもしれません。




上に戻る


より本格的なロボットを求めて

2回に渡る連載でスーパー・カブロボ・コンテストに挑戦するための準備と比較的簡単なプログラム例を紹介してきました。実はまだ SDK のごく一部の機能しか紹介できておりません。今回のスーパー・カブロボ・コンテストでは売買手数料がかかりますが、その大まかな手数料を求めることもできるので、1回の売買で期待できる利益と必要な手数料を比較した上で注文を行うことも可能です。また、より複雑なロジックを実装する場合はスクリーニングのタイミングで銘柄を絞り、注文時には絞り込んだ銘柄を取り出して行うだけにすることで動作時間の制限を回避する、といったことも可能です。

これらの具体的な方法や、それ以外に何か取り上げてほしい内容がありましたら是非ご意見ください。この developerWorks 内で「番外編」として紹介する機会を設けたいと考えています。

最後に、この記事をご覧になっていただいた方のロボットがスーパー・カブロボ・コンテストでご活躍することと、同コンテストを通じて Java のプログラムや技術がより広まっていくことを願っております。





上に戻る


ダウンロード

内容ファイル名サイズダウンロード形式
MyRobot002.java,MyRobot003.javaMyRobot002_003.zip925KBHTTP
ダウンロード形式について


著者について

木村 桂: 日本IBM ソフトウェア事業




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



はいいいえわからない
 


 


54321
大変素晴らしい不充分・不完全である
 


上に戻る


    日本IBMについて プライバシー お問い合わせ