無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

クリック証券アクセスプラグインをαリリース

スクレイピング版ClickClient ver 0.1.3をリリースしました。

Github - unageanu / clickclient_scrap

このバージョンから、jiji用のクリック証券アクセスプラグインをリリース物に追加しています。jijiに加えてclickclient_scrapを環境にインストールして設定することで、クリック証券にアクセスして取引を行うことが可能となります。

注意事項

  • α版です
    • レート情報の収集や発注、決済など基本的な機能が動くことは確認済みですが、連続稼働は3日くらいでテスト不足という見解です。
  • ご利用は自己責任で
    • このプラグインを有効化したjijiでは、クリック証券にアクセスし実際のお金で取引を行ないます。ご利用は自己責任でお願いします。
    • プログラムの不備・不具合等によるあらゆる損害について、作成者は責任を負いません。あしからず。
  • 仕様変更により動かなくなるリスクがあります。
    • スクレイピングを行うライブラリですので、Webサイトの仕様変更により突然動作しなくなるリスクがあります。

jijiへの導入手順

前提条件

クリック証券のアカウントが必要です。インストールの前にご用意ください。

導入

以下の操作を実行します。

$ gem sources -a http://gems.github.com
$ gem install unageanu-clickclient_scrap

導入後、「jiji setting」を実行すればプラグインが認識されているはずです。あとは設定を行ってjijiを再起動すればOK。

$ jiji setting
> Please select a securities.
    1 : CLICK Securities DEMO Trade
    2 : CLICK Securities
2 ←「クリック証券」を選択
> Please input a user name of CLICK Securities.
unageanu ←ユーザー名を入力
> Please input a password of CLICK Securities.
************ ←パスワードを入力
> Please input a proxy. example: https://example.com:80 (default: nil )
←(必要なら)プロキシを入力
> Please input a data directory of jiji. (default: ~/.jiji )

...略


不具合などありましたら、例によってブログのコメント、もしくは GitHubのIssuesで報告いただければ幸いです。

加重移動平均を算出する

移動平均もいろいろあるなー。

Wikipedia - 移動平均

普通の移動平均は作成済みなので、その次の加重移動平均を実装。

# 一定期間の加重移動平均を得る
class WeightedMovingAverage
  def initialize( range=25 )
    @rates = [] # レートを記録するバッファ
    @range = range
  end

  def next_rate( rate )
    # バッファのデータを更新
    @rates.push rate
    @rates.shift if @rates.length > @range
    
    # バッファサイズが十分でなければ、nilを返す。
    return nil if @rates.length != @range
    
    return WeightedMovingAverage.get_weighted_moving_average(@rates)
  end

private

  # 加重移動平均値を計算する。 
  # see http://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87
  def self.get_weighted_moving_average( rates )
    weight = 1
    total = rates.inject(0.0) {|t,s|
      t += s.end * weight
      weight += 1
      t
    }
    return total / ( rates.length * (rates.length+1) / 2 )
  end
end

利用側のエージェントは以下。普通の移動平均との違いがわかるように、両者を表示してみました。実行してみると加重移動平均(緑)のほうが感度が高いのがわかるかと思います。

#
# 加重移動平均を使うエージェントのサンプル
#
class MovingAverageAgent < JIJI::PeriodicallyAgent

  # エージェントの説明
  def description
      "加重移動平均を使うエージェントのサンプル。"
  end
  
  # エージェントを初期化する。
  def init
    # 移動平均の算出クラス
    @mvs = [
      JIJI::Agent::Shared::WeightedMovingAverage.new(@range),
      JIJI::Agent::Shared::MovingAverage.new(@range)
    ]
    @out = output.get( "加重移動平均と移動平均", :graph, {
      :column_count=>2,
      :graph_type=>:rate,      
      :colors=>["#99aaaa","#aa99aa"] # デフォルトのグラフの色
    } )
  end
  
  # 次のレートを受け取る
  def next_period_rates( rates )
    res = @mvs.map{|mv| mv.next_rate( rates[:EURJPY].bid ) }
    return if ( !res[0] || !res[1])
    @out.put( *res )
  end  
  
  # UIから設定可能なプロパティの一覧を返す。
  def property_infos
    super().concat [
      Property.new( "range", "集計期間", 25, :number )
    ]
  end

end

例によって、デモサイトにも置いています。

ボリンジャーバンドを算出する

クリック証券のスクレイピングプラグインもだいぶできつつある(現在稼動テスト&デバッグ中)ので、ぼちぼち組み込みのライブラリを充実させて簡単にエージェントを作れるようにしていきたい。ということで、とりあえずボリンジャーバンドを算出するライブラリを書いてみました。jiji添付の「MovingAverage」と同じような形で使えます。

# ボリンジャーバンドを得る
class BollingerBands
  
  #コンストラクタ
  #range:: 集計期間
  def initialize( range=25, pivot=[0,1,2] )
    @rates = [] # レートを記録するバッファ
    @range = range
    @pivot = pivot
  end

  def next_rate( rate )
    # バッファのデータを更新
    @rates.push rate
    @rates.shift if @rates.length > @range
    
    # バッファサイズが十分でなければ、nilを返す。
    return nil if @rates.length != @range
    
    # ボリンジャーバンドを算出
    return BollingerBands.get_bollinger_bands(@rates, @pivot)
  end

private

  # 移動平均値を計算する。 
  def self.get_moving_average( rates )
    total = 0
    rates.each {|s|
      total += s.end
      total += s.max
      total += s.min
    }
    return total / ( rates.length * 3 )
  end  

  #===ボリンジャーバンド値を得る。
  #
  # TP(ティピカルプライス)=(高値+安値+終値)÷3
  # +2σ=TPの移動平均+標準偏差×2
  # +σ=TPの移動平均+標準偏差
  # -σ=TPの移動平均-標準偏差
  # -2σ=TPの移動平均-標準偏差×2
  # 標準偏差=√{(各TP−TPの期間中平均値)の2乗を期間分全部加えたもの}÷期間
  # (√は式全体にかかる)
  #
  #rates:: 値を取得する範囲の株価の配列 25が一般的。
  #range:: 標準偏差の倍数。初期値[0,1,2]
  #戻り値:: ボリンジャーバンドの各値の配列。例)  [+2σ, +1σ, TP, -1σ, -2σ]
  #
  def self.get_bollinger_bands( rates, range=[0,1,2] )
    ma = get_moving_average( rates )
    total = rates.inject(0.0) {|t,s|
      t+= (( (s.end + s.max + s.min ) / 3 ) - ma ) ** 2
      t
    }
    sd = Math.sqrt((total / rates.length))
    res = []
    range.each { |r|
      res.unshift( ma + sd * r )
      res.push( ma + sd * r * -1 ) if r != 0
    }
    return res
  end
end

利用側のエージェントは以下。取引はせず、グラフデータの出力のみ行っています。

#ボリンジャーバンドを使うエージェントのサンプル
class BollingerBandSampleAgent < JIJI::PeriodicallyAgent

  def description
      <<-STR
ボリンジャーバンドを使うエージェントのサンプルです。
      STR
  end
  
  # エージェントを初期化する。
  def init
    @bollinger_band = JIJI::Agent::Shared::BollingerBands.new(@range)
    
    # 移動平均をグラフで表示するためのOutput
    @out = output.get( "ボリンジャーバンド", :graph, {
      :column_count=>5,
      :graph_type=>:rate, 
      :colors=>["#779999","#557777","#335555","#557777", "#779999"]
    } )
  end
  
  # 次のレートを受け取る
  def next_period_rates( rates )
    res =@bollinger_band.next_rate( rates[:EURJPY].bid )
    return unless res
    
    @out.put( *res )
  end  
  
  # UIから設定可能なプロパティの一覧を返す。
  def property_infos
    super().concat [
      Property.new( "range", "集計期間", 25, :number )
    ]
  end

end

デモサイトにも置いたので遊びたい方はどうぞ。

週末になるとjijiのデモサイトが落ちる

週末になるとjijiのデモサイトが落ちる問題があって、Ubuntuのアップデートとかでシステムが再起動されてるのかなー?とか思っていたんだけど、ログをちゃんとみてみると、OOM Killerが発動して殺されてるじゃないか!!!名前は聞いたことあったけど動いているのははじめてみたよ > OOM Killer。

原因は、まず間違いなくjijiなんだろうけど(申し訳ない)なんで週末にだけメモリ消費が増大するのかがわからん。考えられるのは、クリック証券Webサービスのシステムメンテナンスくらいかなー。メンテナンスに移行するタイミングでなんか変なデータ返してきてんじゃねーの?と言いがかりをつけてみる。 さて、どう調査したものか。

修正しました。→デモサイトに繋がらなくなっています。

デモサイトに繋がらなくなっていたのを修正しました。

原因

サーバーの置き換え後、ルータでのルーティング設定をいじったのですが、変更後ルータを再起動しないと設定が反映されないらしく、それをスルーしていたのが原因です。UIに「反映」ボタンがあったのでそれを押しただけで満足していました・・・。

対処手順

次に更新するときのための自分用メモです。

  1. ルータの設定をいじる。
  2. ルータを再起動する。
  3. IPアドレスが恐らく変わっているのでDynDNS.orgで再登録する。
  4. 確認用スクリプトで外部からのアクセスを確認する。

デモサイトに繋がらなくなっています。

申し訳ありません。現在、jijiのデモサイトに接続できなくなっているようです。
連休中に自宅サーバーを変更したので、どこかで設定をミスっているものと思われます。orz.しばらくお待ちください。

証券会社アクセスプラグイン作成ガイド

jiji 1.1.0をリリースしました。本バージョンより、証券会社へのアクセスロジックをプラグインで後から追加できるようになっています。

オープン・フリーのFX自動取引システム「jiji (ジジ) 」
Github - unageanu/jiji
アクセスプラグイン自体は、まだ「クリック証券デモトレード」用のものしかありませんが、差し替えの仕組みは実装済みです。ということで、証券会社アクセスプラグインの作り方について解説します。

作成手順の概要

  1. 必要な機能を提供するクラスを書く。
  2. jiji_plugin.rb を用意し、プラグインをjijiに登録するコードを書く。
  3. 作成したファイルを含むgemを作成し、配布する。

1.必要な機能を提供するクラスを書く

証券会社アクセスプラグインが必要とする機能を提供するプラグインクラスを作成します。

  • 必要な機能は以下の4つです。
    • 利用可能な通貨ペア一覧の取得
    • 現在のレート(bid,ask,売スワップ、買スワップ)一覧の取得
    • (成り行きでの)売/買注文
    • 決済
  • ↑とは別に「jiji setting」で入力が必要な情報(ユーザー名/パスワード)を返すAPIなども実装する必要があります。

APIの詳細は、以下のモジュールの説明を参照ください。プラグインクラスを作成する場合、このモジュールをincludeして必要なAPIをオーバーライドするのが簡単です。

module JIJI
  module Plugin
    
    #
    #===証券会社アクセスプラグイン
    #
    #証券会社へのアクセスを提供するプラグインのインターフェイスを示すモジュール。
    #証券会社アクセスプラグインはこのモジュールが示すメソッドを実装する必要があります。
    #
    module SecuritiesPlugin
      
      #プラグイン識別子
      FUTURE_NAME = :securities
      
      #プラグインの識別子を返します。
      def plugin_id
      end
      
      #プラグインの表示名を返します。
      #「jiji setting」での証券会社選択時に使用します。
      def display_name
      end
      
      #「jiji setting」でユーザーに入力を要求するデータの情報を返します。
      #return:: JIJI::Plugin::Securities::Inputの配列
      def input_infos 
      end
      
      #プラグインを初期化します。プラグインの利用が開始される前に1度だけ呼び出されます。
      #引数として、ユーザーが入力したパラメータが渡されます。
      #props:: ユーザーが入力したパラメータ(JIJI::Plugin::Securities::Inputのkeyをキーとする設定値の配列)
      #logger:: ロガー
      def init_plugin( props, logger ) 
      end
      
      #プラグインを破棄します。jijiの停止時に1度だけ呼び出されます。
      def destroy_plugin
      end
      
      #利用可能な通貨ペア一覧を取得します。
      #return:: JIJI::Plugin::Securities::Pairの配列
      def list_pairs
      end
      
      #現在のレートを取得します。
      #return:: 通貨ペア名をキーとするJIJI::Plugin::Securities::Rateのハッシュ
      def list_rates
      end
      
      #成り行きで発注を行います。
      #pair:: 通貨ペア名
      #sell_or_buy:: 売(:sell)または買い(:buy)
      #count:: 取引数量
      #return:: JIJI::Plugin::Securities::Position
      def order( pair, sell_or_buy, count )
      end
      
      #建玉を決済します。
      #position_id:: 建玉ID
      #count:: 取引数量
      def commit( position_id, count )
      end
      
      #===ユーザーに入力を要求するデータの情報
      #key:: データのキー
      #description:: 入力時に表示する説明
      #secure:: UIでの入力値の表示を行うかどうか。trueにするとUI上では「*」で表示されます。
      #validator:: UIでの入力値のチェックを行うProc。引数として文字列を受け取り、
      #                 エラーがあった場合はエラーメッセージ、問題ない場合はnilを返すこと。
      #                 nilを指定すると、入力値のチェックを行わない。
      Input = Struct.new( :key, :description, :secure, :validator )
      
      #===取引可能な通貨ペア
      #name:: 通貨ペア名 例) :EURJPY
      #tade_unit:: 取引単位
      Pair = Struct.new( :name, :trade_unit )
      
      #===レート
      #bid:: bidレート
      #ask:: askレート
      #sell_swap:: 売りスワップ
      #buy_swap:: 買いスワップ
      Rate = Struct.new( :bid, :ask, :sell_swap, :buy_swap )
      
      #===建玉
      #position_id:: 建玉の識別子
      Position = Struct.new( :position_id )
    end
end

参考までに、jiji添付の「クリック証券デモトレードアクセスプラグイン」のコードも載せておきます。依存しているクラスなどはGithub - unageanu/jiji を参照ください。

require 'jiji/plugin/securities_plugin'
require 'jiji/plugin/embedded/single_click_client'

module JIJI
  module Plugin
    
    # クリック証券デモトレードアクセスプラグイン
    class ClickSecuritiesDemoPlugin
      include JIJI::Plugin::SecuritiesPlugin
      
      #プラグインの識別子を返します。
      def plugin_id
        :click_securities_demo
      end
      #プラグインの表示名を返します。
      def display_name
        "CLICK Securities DEMO Trade"
      end
      #「jiji setting」でユーザーに入力を要求するデータの情報を返します。
      def input_infos
        [ Input.new( :user, "Please input a user name of CLICK Securities DEMO Trade.", false, nil ),
          Input.new( :password, "Please input a password of CLICK Securities DEMO Trade.", true, nil )]
      end
      
      #プラグインを初期化します。
      def init_plugin( props, logger ) 
        @client = JIJI::Plugin::SingleClickClient.new( props, logger )
      end
      #プラグインを破棄します。
      def destroy_plugin
        @client.close if @client
      end
      
      #利用可能な通貨ペア一覧を取得します。
      def list_pairs
        pairs = @client.request {|fx| fx.list_currency_pairs }
        return pairs.map {|i| 
          name = convert_currency_pair_code(i[0])
          Pair.new( name, i[1].trade_unit )
        }
      end
      
      #現在のレートを取得します。
      def list_rates
        @client.request {|fx|               
          fx.list_rates.inject({}) {|r,p|
            code = convert_currency_pair_code(p[0])
            r[code] = Rate.new( p[1].bid, p[1].ask, p[1].sell_swap, p[1].buy_swap )
            r
          }
        }
      end
      
      #成り行きで発注を行います。
      def order( pair, sell_or_buy, count )
        result = @client.request{ |fx|
            fx.order( convert_currency_pair_code_r(pair),
              sell_or_buy == :buy ? ClickClient::FX::BUY : ClickClient::FX::SELL,  count )
        }
        return JIJI::Plugin::SecuritiesPlugin::Position.new( result.open_interest_no )
      end
      
      #建玉を決済します。
      def commit( position_id, count )
        @client.request {|fx| fx.settle( position_id, count ) }
      end
      
      # 通貨ペアコードをシンボルに変換する
      def convert_currency_pair_code(code)
        case code
          when ClickClient::FX::USDJPY
            return :USDJPY
          when ClickClient::FX::EURJPY
            return :EURJPY
          when ClickClient::FX::GBPJPY
            return :GBPJPY
          when ClickClient::FX::AUDJPY
            return :AUDJPY
          when ClickClient::FX::NZDJPY
            return :NZDJPY
          when ClickClient::FX::CADJPY
            return :CADJPY
          when ClickClient::FX::CHFJPY
            return :CHFJPY
          when ClickClient::FX::ZARJPY
            return :ZARJPY
          when ClickClient::FX::EURUSD
            return :EURUSD
          when ClickClient::FX::GBPUSD
            return :GBPUSD
          when ClickClient::FX::AUDUSD
            return :AUDUSD
          when ClickClient::FX::EURCHF
            return :EURCHF
          when ClickClient::FX::GBPCHF
            return :GBPCHF
          when ClickClient::FX::USDCHF
            return :USDCHF
        end
      end
      
      # シンボルを通貨ペアコードに変換する
      def convert_currency_pair_code_r(code)
        case code
          when :USDJPY
            return ClickClient::FX::USDJPY
          when :EURJPY
            return ClickClient::FX::EURJPY
          when :GBPJPY
            return ClickClient::FX::GBPJPY
          when :AUDJPY
            return ClickClient::FX::AUDJPY
          when :NZDJPY
            return ClickClient::FX::NZDJPY
          when :CADJPY
            return ClickClient::FX::CADJPY
          when :CHFJPY
            return ClickClient::FX::CHFJPY
          when :ZARJPY
            return ClickClient::FX::ZARJPY
          when :EURUSD
            return ClickClient::FX::EURUSD
          when :GBPUSD
            return ClickClient::FX::GBPUSD
          when :AUDUSD
            return ClickClient::FX::AUDUSD
          when :EURCHF
            return ClickClient::FX::EURCHF
          when :GBPCHF
            return ClickClient::FX::GBPCHF
          when :USDCHF
            return ClickClient::FX::USDCHF
        end
      end
      
    end
    
  end
end

2. jiji_plugin.rb を用意し、プラグインをjijiに登録するコードを書く。

プラグインクラスができたら、jijiにプラグインを認識させるためのRubyスクリプト「jiji_plugin.rb」を書きます。

  • jijiは、起動時にすべてのgemに含まれる「lib/jiji_plugin.rb」ファイルを探索し、ロードします。(詳細はプラグインローダーの解説を参照ください。)
  • 「jiji_plugin.rb」内にプラグイン登録コードを書いておくことで、プラグインがjijiに認識されるようになります。

詳細は以下のコードを参照ください。

# 必要なモジュール(プラグインクラスなど)を読み込む
require 'jiji/plugin/embedded/click_securities_demo_plugin'

# 「JIJI::Plugin.register( <機能名>, <プラグインインスタンス>)」でjijiに機能を登録する。
# 以下は、証券会社アクセスプラグインとして、「JIJI::Plugin::ClickSecuritiesDemoPlugin」を登録する例
JIJI::Plugin.register( 
  JIJI::Plugin::SecuritiesPlugin::FUTURE_NAME, 
  JIJI::Plugin::ClickSecuritiesDemoPlugin.new )

3.作成したファイルを含むgemを作成し、配布する。

プラグインクラス、および「jiji_plugin.rb」を含むgemを作成します。

  • プラグインクラスは任意の場所に、
  • 「jiji_plugin.rb」は「lib」直下に配置する必要があります。

あとは、gemをインストールしてjijiを再起動すれば、プラグインが認識されるはずです。正しく認識されていれば、「jiji setting」で利用可能な証券会社の選択肢が追加されます。プラグインを公開したい場合は、gemを公開すればOK。rubyforgegithubでの配布ももちろん可能です。



解説は以上。不具合などありましたら、blogのコメントなりGithubのissuesなりでご報告下さい。