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

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

グラフの設定が保存されない不具合を修正したjiji-1.2.5をリリース

グラフの設定が保存されない不具合にいまさら気がついたので、修正版をリリースします。
Github - unageanu/jiji
Gemcutter.org - jiji

GitHubのgemビルド機能が廃止になったようなので、Gemcutterにアップロードしています。gemの名前も「unagenau-jiji」→「jiji」に修正。初めて使う場合は

$ gem install gemcutter
$ gem tumble

してから、以下のコマンドを実行してください。

$ jiji stop
$ gem uninstall unageanu-jiji
$ gem install jiji
$ jiji start

クリック証券で提供されている為替レートのヒストリカルデータをjijiに取り込むスクリプト

クリック証券で提供されているヒストリカルデータをダウンロードして、jijiで使えるCSV形式にコンバートするツールを作ってみました。

使い方

$ ruby rate_data_importer.rb <クリック証券のユーザーID> <パスワード> <取り込む年> <開始月> <終了月>

具体例。

$ ruby rate_data_importer.rb foo var 2007 1 2
  • 実行すると、「./tmp」にヒストリカルデータが展開され、「./converted」に変換済みデータが作成されます。
  • あとは、「./converted」以下のフォルダをjijiのレートデータ置き場(デフォルトでは~/.jiji/rate_datas)に配置すれば、チャートやバックテストで利用可能となります。

注意事項

  • クリック証券のアカウントが必要です。また、ヒストリカルデータの利用規約もご確認願います。
  • スワップは売り/買い共に0,スプレッドは現在の値を固定で使用しています。この点はリアルなデータと違うのでご了承ください。
  • データコンバートにかなり時間がかかるので(Core2Duo 2.53GHz で1ヶ月90分くらい)、気長にお待ちください。

ツールのコード

以下です。ちょい長いのでご注意。

require 'kconv'
require 'rubygems'
require 'mechanize'
require 'zip/zip'
require 'tmpdir'
require 'jiji/registry'

module JIJI

  #
  #==クリック証券のヒストリカルデータダウンロードサービスから為替レートデータを取得するユーティリティ
  #
  module Download

    #===ダウンロードを行うためのセッションを開始する
    #userId:: クリック証券のユーザーID
    #password:: ログインパスワード
    #proxy:: プロキシ
    def self.session( userid, password, proxy=nil )
      client = WWW::Mechanize.new {|c|
        # プロキシ
        if proxy
          uri = URI.parse( proxy )
          c.set_proxy( uri.host, uri.port )
        end
      }
      client.keep_alive = false
      client.max_history=0
      client.user_agent_alias = 'Windows IE 7'

      # ログイン
      page = client.get("https://sec-sso.click-sec.com/loginweb/")
      raise "Unexpected Error" if page.forms.length <= 0
      form = page.forms.first
      form.j_username = userid
      form.j_password = password
      client.submit(form, form.buttons.first)
      session = Session.new( client )
      if block_given?
        begin
          return yield( session )
        ensure
          session.logout
        end
      else
        return session
      end
    end
    class Session
      def initialize( client )
        @client = client
      end
      #===CSVデータをダウンロードする
      #yesr:: 年
      #month:: 月
      #pair:: 通貨ペア
      #to:: ダウンロード先ディレクトリ
      def download( year, month, pair, to="./" )
        FileUtils.makedirs(to)
        file = "#{to}/#{pair}_#{year}_#{month}.zip"
        result = @client.get("https://tb.click-sec.com/fx/historical/historicalDataDownload.do?" +
          "y=#{year}&m=#{sprintf("%02d", month)}&c=#{C_MAP[pair]}&n=#{pair}" )
        open( file, "w" ) {|w| w << result.body }
        extract( file, "#{to}" )
        FileUtils.rm(file)
      end
      #===ログアウトする
      def logout
        @client.get("https://sec-sso.click-sec.com/loginweb/sso-logout")
      end
      #===zipファイルを展開する。
      #zip:: zipファイル
      #dest:: 展開先ディレクトリ
      def extract( zip, dest )
        FileUtils.makedirs(dest)
        Zip::ZipFile.foreach(zip) {|entry|
          if entry.file?
            FileUtils.makedirs("#{dest}/#{File.dirname(entry.name)}")
            entry.get_input_stream {|io|
              open( "#{dest}/#{entry.name}", "w" ) {|w|
                while ( bytes = io.read(1024))
                  w.write bytes
                end
              }
            }
          else
            FileUtils.makedirs("#{dest}/#{entry.name}")
          end
        }
      end
      C_MAP = {
        :USDJPY=>"01", :EURJPY=>"02", :GBPJPY=>"03",
        :AUDJPY=>"04", :NZDJPY=>"05", :CADJPY=>"06",
        :CHFJPY=>"07", :ZARJPY=>"08", :EURUSD=>"09",
        :GBPUSD=>"10", :AUDUSD=>"11",:EURCHF=>"12",
        :GBPCHF=>"13", :USDCHF=>"14"
      }
    end
    PAIRS = [        
      :USDJPY, :EURJPY, :GBPJPY, :AUDJPY, :NZDJPY, :CADJPY,
      :CHFJPY, :ZARJPY, :EURUSD, :GBPUSD, :AUDUSD,:EURCHF,
      :GBPCHF, :USDCHF
    ]
  end

  class Converter

    def initialize( )
      @registry = JIJI::Registry.new( "./" )
    end

    #===展開したCSVデータをjijiの形式にフォーマットする。
    #csv_dir:: csvデータ置き場
    #to:: CSVデータ
    def convert( dir, to )
      FileUtils.mkdir_p to
      dao = @registry.rate_dao
      dao.instance_variable_set(:@data_dir, to)
      # CSVを読みつつデータを作成
      each_rate(dir) {|rates|
        dao.next_rates( rates )
      }
    end
    def each_rate(dir, &block)
      yyyymm = Dir.entries( dir ).reject{|d| !(d=~ /\d{6}/)  }.sort
      yyyymm.each {|ym|
        1.upto(31).each {|d|
          readers = {}
          begin 
            JIJI::Download::Session::C_MAP.each {|p|
              file = "#{dir}/#{ym}/#{p[0]}_#{ym}#{sprintf("%02d", d)}.csv"
              next unless File.exist? file
              readers[p[0]] = PushBackReader.new(CSV.open( file, 'r' ))
              readers[p[0]].shift # 最初のデータはヘッダーなので除外
            }
            next if readers.empty?
            read( readers, &block )
          ensure
            readers.each {|p| 
              begin
                p[0].close
              rescue; end
            }
          end
        }
      }
    end
    def read( readers )
      while( true )
        first = nil
        readers.each {|p|
          line = p[1].shift
          p[1].unshift line
          next if !line || line.length < 5
          next unless line[0] =~ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/
          first = !first || first.to_i > line[0].to_i ? line[0] : first
        }
        return unless first
        buff = readers.inject([]) {|r,p|
          line = p[1].shift
          next r if !line || line.length < 5
          if ( line[0] != first)
            p[1].unshift line
            next r
          end
          0.upto(3) {|i|
            time = Time.local( $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, i*10 )
            map = r[i] || r[i] = {}
            bid = line[i+1].to_f
            map[p[0].to_sym] = Rate.new( bid, bid+SPREAD[p[0].to_sym], 0, 0, time )
          }
          r
        }
        return if !buff || buff.empty?
        buff.each {|e|
          yield Rates.new( {}, e, e[:USDJPY].time )
        }
      end
    end
    SPREAD = {
      :AUDJPY => 0.02, :GBPUSD => 0.0003, :NZDJPY => 0.024,
      :AUDUSD => 0.0003, :CADJPY => 0.03, :EURCHF => 0.0003,
      :USDJPY => 0.008, :CHFJPY => 0.03,:GBPCHF => 0.0004, 
      :EURJPY => 0.014,:ZARJPY => 0.04,:USDCHF => 0.0004,
      :GBPJPY => 0.024,:EURUSD => 0.00016
    }
  end
  class PushBackReader
    def initialize( reader )
      @reader = reader
      @buff = []
    end
    def shift
      @buff.empty? ? @reader.shift : @buff.shift
    end
    def unshift(v)
      @buff.unshift v
    end
    def close
      @reader.close
    end
  end
end

puts "start download. #{Time.now}"
JIJI::Download.session( ARGV[0], ARGV[1] ) {|s|
  ARGV[3].to_i.upto( ARGV[4].to_i ) {|month|
  JIJI::Download::PAIRS.each {|pair|
      s.download( ARGV[2], month, pair, "./tmp" )
      puts "downloaded. #{ARGV[2]}/#{sprintf("%02d", month)} #{pair}"
    }
  }
}
puts "end download. #{Time.now}"
puts "start convert. #{Time.now}"
converter = JIJI::Converter.new
converter.convert( "./tmp", "./converted" )
puts "end convert. #{Time.now}"

細かい機能追加を行ったjiji-1.2.4をリリース

細かい機能追加を行ったjiji-1.2.4をリリースしました。
Github - unageanu/jiji
前バージョンからの変更点は以下です。

  • 取引結果の約定日時/決済日時から、その時点のチャートにジャンプする機能を追加しました。
  • チャート上で取引の詳細を表示する際に、表示中の取引を強調表示するように修正しました。

あと、JSON-RPC APIを利用してバックテストの実行と結果の取得を行うユーティリティもリリース物に追加しています。

更新手順

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

$ gem update unageanu-jiji

バックテストを実行して結果を取得するサンプル

jijiにバックテストを登録&実行して、結果を取得するスクリプトのサンプルです。

  • デモサイトに接続して、
  • パラメータの違う移動平均エージェントを2つ実行するバックテストを作成。
  • 実行終了を待ち、結果を出力します。

いろいろいけてないところを隠蔽するため、ユーティリティクラスを作ってみました。(ユーティリティのソースは末尾)

require 'backtester' # テストを実行するためのユーティリティクラス

#テスターを作成
tester = JIJI::BackTester.new( "http://unageanu.homeip.net/jiji-demo"  )

#エージェントを作成
agents = []
agents << tester.create_agent( "MovingAverageAgent", "moving_average_agent.rb",
  "移動平均_12-36", {"long"=>36,"period"=>10,"short"=>12})
agents << tester.create_agent( "MovingAverageAgent", "moving_average_agent.rb",
  "移動平均_25-75", {"long"=>75,"period"=>10,"short"=>25})

#テストを実行
time = Time.local(2009, 8, 20).to_i
process_id = tester.regist_test( "移動平均テスト2", "", time, time+24*60*60*3, agents )

#テストの実行完了を待つ
tester.wait( process_id )

#結果を確認
result = tester.get_result( process_id )
result.each_pair {|k,v|
  puts "\n---#{k}"
  puts "profit or ross : #{v.profit_or_loss}" 
  puts "positions : "
  v.positions.each {|p|
    puts "  #{p["pair"]} #{p["sell_or_buy"]} #{p["profit_or_loss"]}"
  }
}

実行結果です。

---移動平均_25-75
profit or ross : -4513
positions : 
  EURJPY sell -4900 
  EURJPY sell -100 
  EURJPY sell -3800 
  EURJPY buy 10094 
  EURJPY buy -5807 

---移動平均_12-36
profit or ross : -5711
positions : 
  EURJPY buy -100 
  EURJPY buy 5594 
  EURJPY sell -6300 
  EURJPY sell 900 
  EURJPY sell -1599 
  EURJPY sell -100 
  EURJPY buy -1699 
  EURJPY buy -1807 
  EURJPY sell -500 
  EURJPY buy -100 

ユーティリティのソース

次のとおり。

require "rubygems"
require "jiji/util/json_rpc_requestor"
require 'uuidtools'

module JIJI
  #==jijiに接続してバックテストの実行と結果の取得を行うためのユーティリティ。
  class BackTester
    
    #===コンストラクタ
    #endpoint:: 接続先サーバーを示すエンドポイント 例) "http://unageanu.homeip.net/jiji-demo"
    def initialize( endpoint )
      @agent_service = JSONBroker::JsonRpcRequestor.new("agent", endpoint)
      @process_service = JSONBroker::JsonRpcRequestor.new("process", endpoint)
      @trade_result_service = JSONBroker::JsonRpcRequestor.new("trade_result", endpoint)
      @agents = @agent_service.list_agent_class
    end
    #===エージェントの情報を取得する。
    #class_name:: クラス名
    #file:: クラスが定義されているファイル名
    #return:: エージェントの情報/対応するエージェントがなければnil
    def get_agent_info( class_name, file )
      return @agents.find{|i|
        i["class_name"]==class_name && i["file_name"]==file
      }
    end
    
    #===バックテストに登録するためのエージェント情報を作成する。
    #class_name:: クラス名
    #file:: クラスが定義されているファイル名
    #name:: エージェント名
    #properties:: エージェントのプロパティ
    #return:: バックテストに登録するためのエージェント情報
    def create_agent( class_name, file, name,  properties )
      info = get_agent_info( class_name, file )
      raise "agent not found." unless info
      agent = info.dup
      agent["id"] = UUIDTools::UUID.random_create().to_s
      agent["class"] = "#{info["class_name"]}@#{info["file_name"]}"
      agent["name"] = name
      agent["property_def"] = agent["properties"].inject({}){|r,i|
        r[i["id"]] = i
        r
      }
      agent["properties"] = properties
      return agent
    end
    #===バックテストを実行する。
    #title:: テストの名前
    #memo:: メモ
    #start_time:: 開始日時(UNIXタイム/1970-01-01からの秒数)
    #end_time:: 終了日時(UNIXタイム/1970-01-01からの秒数)
    #agents:: 実行するエージェントの配列
    #return:: バックテストの識別ID(process_id)
    def regist_test( title, memo, start_time, end_time, agents )
      return @process_service.new_test( 
        title, memo, start_time, end_time, agents )["id"]
    end
    #===バックテストの完了を待つ
    #process_id:: バックテストの識別ID
    def wait( process_id ) 
      while ( true )
        status = @process_service.status( [process_id] )[0]["state"]
        return if status == "ERROR_END" || status == "FINISHED" || status == "CANCELED"        
        sleep 5
      end
    end
    #===バックテストの実行結果を取得する。
    #process_id:: バックテストの識別ID
    #return:: エージェント名をキーとする実行結果のハッシュ
    def get_result( process_id )
      result = @trade_result_service.list( process_id, "5d", nil, nil )
      return result.inject({}) {|r, item|
        info = r[item["trader"]] ||= Result.new( 0, [] )
        info.profit_or_loss += item["profit_or_loss"]
        info.positions << item
        r
      }
    end
    #実行結果
    Result = Struct.new( :profit_or_loss, :positions )
  end
end

バックテストを登録するスクリプトのサンプル

エージェントのバックテストでは、各種パラメータの組み合わせをいろいろと変えて動作を調整したい場合があります。(カーブフィティングとかいうやつてずね。)jijiでは複数のエージェントを一括でテスト可能ですが、UIからちまちま登録するのはなかなかメンドイスクリプトで、for文とか使ってさくっと一括登録したい。
→ならJSONインターフェイスを使って登録すればいいんじゃね?ということで、jijiにバックテストを登録するRubyスクリプトのサンプルを書いてみました。

  • 短期移動平均(10,20,30)x長期移動平均(30,40,50)のすべての組み合わせの移動平均エージェントを実行するバックテストを、デモサイトに登録するサンプルです。
  • 処理の流れは以下のとおりです。
    1. jiji添付のクライアントライブラリを使用して、jijiに接続するためのスタブクラスを作成。
    2. スタブを利用して、エージェント一覧を取得
    3. 一覧から実行するエージェントを探し、実行時の引数として渡すエージェント一覧を作成。
    4. テスト名、メモ、開始日時、終了日時、↑で作成したエージェントの一覧を指定してバックテストを実行します。
require "rubygems"
require "jiji/util/json_rpc_requestor"
require 'uuidtools'

END_POINT =  "http://unageanu.homeip.net/jiji-demo"

#1.jiji添付のクライアントライブラリを使用して、jijiに接続するためのスタブクラスを作成。
agent_service = JSONBroker::JsonRpcRequestor.new("agent", END_POINT)
process_service = JSONBroker::JsonRpcRequestor.new("process", END_POINT)

#2.エージェント一覧を取得し、登録対象とするエージェントを探索
ma = agent_service.list_agent_class.find{|i|
  i["class_name"]=="MovingAverageAgent" && i["file_name"]=="moving_average_agent.rb"
}
#3.実行するエージェント情報を作成。
#  短期移動平均→10,20,30
#  長期移動平均→40,50,60
#で、3x3の組み合わせをすべて試す。
agents = []
[10,20,30].each {|short| # 短期移動平均のバリエーション
  [40,50,60].each {|long| # 長期移動平均のバリエーション
    #登録情報を作成。
    agent = ma.dup
    agent["id"] = UUIDTools::UUID.random_create().to_s
    agent["class"] = "#{agent["class_name"]}@#{agent["file_name"]}" #いけてない・・。
    agent["name"] = "移動平均_#{short}-#{long}"
    #コンバートしないと実行結果からエージェントの詳細が見えなかったり・・いけてない。
    agent["property_def"] = agent["properties"].inject({}){|r,i|
      r[i["id"]] = i
      r
    }
    agent["properties"] = {"long"=>long,"period"=>10,"short"=>short}
    agents << agent
  }
}

#4.テスト名、メモ、開始日時、終了日時、エージェントを指定してバックテストを実行。
time = Time.local(2009, 8, 20).to_i
process_service.new_test( "バックテスト登録のテスト", "メモ", time, time+12*60*60, agents )

もともと公開APIとして考えていなかったのでいろいろといけてない部分もありますが・・。ご容赦。

エージェントエディタを一新したjiji-1.2.0をリリース

エージェントエディタの一新など、いくつかの機能強化、バグフィクスを行ったjiji-1.2.0をリリースしました。
Github - unageanu/jiji

更新内容、更新手順については、こちらを参照ください。


さて、昨日嵌っていたgemがビルドされない問題は、どうもリポジトリが壊れてしまっていたことが原因だったようです。pushはできるがcloneはできない状態になっていて、エラーメッセージを頼りにgoogleで検索して修復を試みるもうまくいかず、結局新しいリポジトリを新規に作成することにしました。

  1. 元のリポジトリは、jiji_old にリネームし、
  2. 新規にjijiを作成。
  3. ローカルのファイル一式を再コミットしています。

コミットログはすべて失われてしまいましたが、まぁ、やむなしと判断。新規のリポジトリでは無事にgemが作成されました。

エージェントエディタを一新したjiji-1.2.0をリリース、、ならず。

エージェントエディタの一新など、いくつかの機能強化、バグフィクスを行ったjiji-1.2.0をリリース、、、といいたいところですが、GitHubにコミットしたものの1時間待ってもgemがビルドされず・・・・。

オープン・フリーのFX自動取引システム「jiji」
Github - unageanu/jiji

んー、メンテナンス中とかなのかな?しばらく様子をみて、配布が確認できたら別途報告します。

変更点

  • エージェントエディタの一新
    • 複数のエージェントや共有ライブラリをタブで同時に編集できるようになりました。
    • エージェントや共有ライブラリをディレクトリで分類できるようになりました。
  • 標準ライブラリの追加
    • 標準添付の共有ライブラリとして、以下を追加しました。
      • 移動平均などの各種シグナル算出クラス
      • ロスカットやトレーリングストップを容易に実現するPositionManager
      • クロスアップ、クロスダウンを判定するためのユーティリティ
  • バックテストの再実行機能を追加
    • バックテストを1クリックで再実行できるようになりました。
  • グラフ出力の不具合修正
    • エージェントを削除するとそのグラフも表示できなくなる問題を改修しました。
    • この変更でリアルトレードでは古いグラフが蓄積されていくようになったため、不要なグラフを破棄する機能も追加しました。

エージェントエディタはCodePressを捨て、EditAreaに変更。タブでの複数ファイル同時編集やフルスクリーンモード、検索機能がついてちょっと高機能になっています。動作もCodePressよりは早い印象(それでもやや野暮ったいですが)。どうしても不満という場合は、「Ctrl+h」でシンタックスハイライトをoffにすると、大分改善されます。

更新手順

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

$ gem update unageanu-jiji
$ jiji setting

※新たに追加された共有ライブラリをコピーするため、今回は「jiji setting」の再実行も必要です。「jiji setting」での入力内容はインストール手順を参照ください。