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

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

クリック証券デモ取引プラグインを無効化したjiji-1.1.4をリリース

クリック証券デモ取引のWebサービス提供終了を受けて、クリック証券デモ取引プラグインを無効化したjiji-1.1.4をリリースしました。

Github - unageanu/jiji

変更点

  • クリック証券デモ取引プラグインを無効化。
  • マニュアルの「サポートしている証券会社」からも除外しました。

更新手順

以下の操作を実行してください。

$ gem update unageanu-jiji

クロスアップ/クロスダウンを判定するユーティリティ

先行指標と遅行指標を受け取って、クロスアップ/クロスダウンを判定するユーティリティです。利用例と挙動の説明は以下。

require 'cross'

cross = Cross.new

# 先行指標、遅行指標を受け取って状態を返す。
# :cross .. クロスアップ、クロスダウン状態かどうかを返す。
#             - クロスアップ(:up)
#             - クロスダウン(:down)
#             - どちらでもない(:none)
# :trend .. 現在の指標が上向きか下向きかを返す。
#           「先行指標 <=> 遅行指標」した値。
#           trend >= 1なら上向き、trned <= -1なら下向き
p cross.next_data( 100, 90 )  # {:trend=>1, :cross=>:none}
p cross.next_data( 110, 100 ) # {:trend=>1, :cross=>:none}
p cross.next_data( 100, 100 ) # {:trend=>0, :cross=>:none}
p cross.next_data( 90, 100 )  # {:trend=>-1, :cross=>:down}
p cross.next_data( 80, 90 )   # {:trend=>-1, :cross=>:none}
p cross.next_data( 90, 90 )   # {:trend=>0, :cross=>:none}
p cross.next_data( 100, 100 ) # {:trend=>0, :cross=>:none}
p cross.next_data( 110, 100 ) # {:trend=>1, :cross=>:up}

ソースは次の通り。

#===交差状態を判定する
class Cross

  #コンストラクタ
  def initialize
    @cross_prev = nil
    @cross = :none
    @trend = 0
  end

  # 次の値を渡す
  #fast:: 先行指標
  #lazy:: 遅行指標
  def next_data( fast, lazy )
    return unless fast && lazy
    # 交差状態を算出
    @trend = fast <=> lazy
    if @cross_prev && @trend != @cross_prev && @trend != 0
      @cross = @trend > @cross_prev ? :up : :down
    else
      @cross = :none
    end
    @cross_prev = @trend
    return {:cross=>@cross,:trend=>@trend}
  end

  #クロスアップ状態かどうか判定する
  #「先行指標 < 遅行指標」 から 「先行指標 > 遅行指標」 になったらtrue
  def cross_up?
    @cross == :up
  end
  #クロスダウン状態かどうか判定する
  #「先行指標 > 遅行指標」 から 「先行指標 < 遅行指標」 になったらtrue
  def cross_down?
    @cross == :down
  end
  #上昇トレンド中かどうか判定する
  #「先行指標 > 遅行指標」 ならtrue
  def up?
    @trend > 0
  end
  #下降トレンド中かどうか判定する
  #「先行指標 < 遅行指標」 ならtrue
  def down?
    @trend < 0
  end

  #交差状態( :up, :down, :none )
  attr_reader :cross
  #トレンド ( 直近の falst <=> lazy 値。)
  attr_reader :trend
end

ver1.2で添付する予定です。

プロキシ設定の不具合を改修したjiji-1.1.3をリリース

ey さんよりご報告頂いた(ありがとうございます!)プロキシ設定の不具合を改修し、jiji-1.1.3としてリリースしました。

Github - unageanu/jiji

変更点

  • クリック証券のデモ取引に接続する場合に、設定でプロキシを空のまま確定すると接続エラーになる不具合を改修。
  • マニュアルの誤記についてもあわせて修正しています。

更新手順

以下の操作を実行してください。

$ gem update unageanu-jiji

jijiにクリック証券プラグインを標準添付しました

メモリリークも改修できたようなので、クリック証券プラグインを標準添付としたものをjiji-1.1.2としてリリースしました。

Github - unageanu/jiji

といっても変更点は、依存モジュールにunageanu-clickclient_scrapを追加しただけです。あと、解説サイトの内容も修正しています。

更新手順

以下の操作を実行してください。

$ gem update unageanu-jiji

追加された依存モジュール「unageanu-clickclient_scrap」もダウンロードされてきます。更新後、

$ jiji setting

で、アクセス先証券会社として「クリック証券」が追加されているはずです。

RSIを算出する

RSIを算出するクラスです。

module Signal

  #===一定期間のレートデータを元に値を算出するシグナルの基底クラス
  class RangeSignal
    include Signal
    def initialize( range=25 )
      @datas = [] # レートを記録するバッファ
      @range = range
    end
    def next_data( data )
      # バッファのデータを更新
      @datas.push data
      @datas.shift if @datas.length > @range

      # バッファサイズが十分でなければ、nilを返す。
      return nil if @datas.length != @range

      # 算出
      return calculate(@datas)
    end
    def calculate(datas); end
     attr_reader :range
  end

  #===RSI
  class RSI < RangeSignal
    def initialize( range=14 )
      super(range)
    end
    def calculate(datas)
      rsi( datas )
    end
  end

 module_function 
  #===RSIを得る。
  #   
  #RSI = n日間の値上がり幅合計 / (n日間の値上がり幅合計 + n日間の値下がり幅合計) * 100
  #nとして、14や9を使うのが、一般的。30以下では売られすぎ70以上では買われすぎの水準。
  #
  #datas::  値の配列
  #戻り値:: RSI値
  def rsi( datas )
    prev = nil
    tmp = datas.inject( [0.0,0.0] ) {|r,i|
      r[ i > prev ? 0 : 1 ] += (i - prev).abs if prev
      prev = i
      r
    }
    (tmp[0] + tmp[1] ) == 0 ? 0.0 : tmp[0] / (tmp[0] + tmp[1]) * 100
  end
end

RSIを表示するエージェントは以下。

#RSIを表示するエージェントのサンプル
class RSISampleAgent < JIJI::PeriodicallyAgent

  def description
    "RSIを表示するエージェントのサンプルです"
  end
  
  def init
    @rsi = JIJI::Agent::Shared::Signal::RSI.new(@range)
    @out = output.get( "RSI", :graph, {
      :column_count=>1,
      :lines=>[30,70],
      :colors=>["#557777"]
    } )
  end
  
  # 次のレートを受け取る
  def next_period_rates( rates )
    res =@rsi.next_data( rates[:EURJPY].bid.end )
    return unless res
    
    @out.put( res )
  end  
  
  # UIから設定可能なプロパティの一覧を返す。
  def property_infos
    super().concat [
      Property.new( "range", "集計期間", 25, :number )
    ]
  end

end

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

uuidtools 2.0.0に対応したjiji-1.1.1をリリース

「uuidtools 2.0.0 と組みあわせて使うとバックテスト実行時にエラーになる」というご報告を頂いたので(ありがとうございます!)、それを改修したjiji-1.1.1をリリースしました。

Github - unageanu/jiji

問題の詳細は以下を参照ください。また、これに加えて、クリック証券デモトレードプラグンイでプロキシの指定ができるようにもなっています。(ローカルで実装していたので、ついでにリリース。)

更新手順

以下の操作を実行してください。

$ gem update unageanu-jiji

uuidtools 2.0.0 との組み合わせでエラーになる件

問題

jijiをインストールしてバックテストを実行すると、以下のエラーが発生する。

undefined method `random_create' for UUID:Class : /usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/process_manager.rb:89:in `create_back_test'
/usr/lib/ruby/gems/1.8/gems/needle-1.3.0/lib/needle/interceptor-chain.rb:74:in `__send__'
/usr/lib/ruby/gems/1.8/gems/needle-1.3.0/lib/needle/interceptor-chain.rb:74:in `process_next'
/usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/util/synchronize_interceptor.rb:26:in `process'
/usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/util/synchronize_interceptor.rb:25:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/util/synchronize_interceptor.rb:25:in `process'
/usr/lib/ruby/gems/1.8/gems/needle-1.3.0/lib/needle/interceptor-chain.rb:58:in `process_next'
(eval):3:in `create_back_test'
/usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/service/process_service.rb:31:in `new_test'
/usr/lib/ruby/gems/1.8/gems/unageanu-jiji-1.1.0/lib/jiji/
原因

uuidtools 2.0.0 より「UUID」クラスが「UUIDTools::UUID」に移動となったことが原因です。(jijiとは関係ありませんが、このサイトに同様の問題が報告されています。)

本来は、gemの依存モジュール設定で互換性のない依存ライブラリが使われないようにしておくべきですが、上位互換は維持されるだろうという楽観論に基づき「'1.0.7'以降のuuidtoolsに依存」としていました。このため、uuidtoolsとjijiが共にインストールされていない環境に新規インストールすると、最新の2.0.0が使われエラーになります。

対応

せっかくなのでuuidtools 2.0.0にあわせてjijiでの呼び出し箇所を修正して対応しました。また、他に同様の問題が発生しないよう、依存モジュールのバージョンは固定としました。

指数移動平均を算出する

指数移動平均を算出するライブラリです。

  • ちょっとリファクタして、移動平均加重移動平均と共通部分を親クラスに移動しています。ファイルも1つに統合して、APIも微妙に修正してます。
  • あと、加重移動平均の加重が逆(古いものを重視)だったのも修正してます。orz.
module Signal

  #===一定期間のレートデータを元に値を算出するシグナルの基底クラス
  class RangeSignal
    def initialize( range=25 )
      @datas = [] # レートを記録するバッファ
      @range = range
    end
    def next_data( data )
      # バッファのデータを更新
      @datas.push data
      @datas.shift if @datas.length > @range
  
      # バッファサイズが十分でなければ、nilを返す。
      return nil if @datas.length != @range
  
      # 算出
      return calculate(@datas)
    end
    def calculate(datas)
    end
  end
  
  #===移動平均
  class MovingAverage < RangeSignal
    def calculate(datas)
      MovingAverage.ma( datas )
    end
    # 移動平均値を計算する。
    def self.ma( datas )
      total = datas.inject {|t,s|
        t += s; t
      }
      return total / datas.length
    end
  end
  
  #===加重移動平均
  class WeightedMovingAverage < RangeSignal
    def calculate(datas)
      WeightedMovingAverage.wma( datas )
    end
    # 加重移動平均値を計算する。
    def self.wma( datas )
      weight = 1
      total = datas.inject(0.0) {|t,s|
        t += s * weight
        weight += 1
        t
      }
      return total / ( datas.length * (datas.length+1)  /2 )
    end
  end
  
  #===指数移動平均
  class ExponentialMovingAverage < RangeSignal
    def initialize( range=25, smoothing_coefficient=0.1 )
      super(range)
      @sc = smoothing_coefficient
    end
    def calculate(datas)
      ExponentialMovingAverage.ema( datas, @sc )
    end
    # 指数移動平均値を計算する。
    def self.ema( datas, smoothing_coefficient )
      datas[1..-1].inject( datas[0] ) {|t,s|
        t + smoothing_coefficient * (s - t)
      }
    end
  end

end

移動平均加重移動平均、指数移動平均のグラフを表示するエージェントのサンプルです。

#
# 移動平均をいろいろ試してみるエージェントのサンプル
#
class MovingAveragesAgent < JIJI::PeriodicallyAgent

  include JIJI::Agent::Shared::Signal
  
  # エージェントの説明
  def description
      "移動平均、加重移動平均、指数移動平均を表示する"
  end
  
  # エージェントを初期化する。
  def init
    @mvs = [
      MovingAverage.new(@range),
      WeightedMovingAverage.new(@range),
      ExponentialMovingAverage.new(@range)
    ]
    @out = output.get( "移動平均,加重移動平均,指数移動平均", :graph, {
      :column_count=>3,
      :graph_type=>:rate,      
      :colors=>["#8899aa","#aa8899","#99aa88"] 
    } )
  end
  
  # 次のレートを受け取る
  def next_period_rates( rates )
    res = @mvs.map{|mv| mv.next_data( rates[:EURJPY].bid.end ) }
    return if ( !res[0] || !res[1] || !res[2] )
    @out.put( *res )
  end  
  
  # UIから設定可能なプロパティの一覧を返す。
  def property_infos
    super().concat [
      Property.new( "range", "集計期間", 25, :number )
    ]
  end

end