みなさん、こんにちわ。
マネックス証券システム開発部の平井です。
最近はApache Kafkaとチョコレートバナナパフェにハマっています。
はじめに
仕事ではマネックス証券の証券システムを開発しているのですが、個人的にシステムトレードやプログラム売買と呼ばれているようなトレードをするプログラムを作ることにも興味があります。
なんで興味があるかと言うと、学生時代にゲームをプレイするAIプログラムを研究していたのですが、人間がやることをプログラムで攻略することにゾクゾク・ワクワクするからだと思います(プログラムが人間よりも上手ければ最高です)。
トレードプログラムの目標は明確です。
トレードで利益を得ることです。
ゲームはルールが明確で、勝ち負けにより良し悪しを判断できるため、人工知能の性能評価に用いられてきたのですが、トレードプログラムにおいても新規注文と返済注文のワンセットで損益が確定するため、ゲームと同様に性能の評価がしやすいです。
学生時代に「究極のゲームはなんだろう」という疑問を持っていて、社会人になってからすっかり忘れていたのですが、2017年に仮想通貨(暗号資産)に出会い、答えが出ました笑
今回は仮想通貨を題材に仮想トレードプログラムに挑戦したので、その内容について紹介します。
具体的には Coincheck APIを利用してBTC/JPYのマーケットで仮想トレードするプログラムについて紹介します。
とはいえ、まだ研究段階なので現金を使って損をするのは避けたいです。そこで、Coincheck APIを利用して BTC/JPYの価格を取得して、トレードプログラム内でトレードをしたと仮定します。
事前準備
Coincheck API を利用したトレードプログラムをつくるには、まずAPIキーが必要です。
APIキーの取得方法についてはこちらをご参照ください。
faq.coincheck.com
Coincheck APIを利用する際に役立つライブラリがCoincheckから配布されています。
対応言語はPHP、Ruby、Python、Node、Java、C#、Goのようです。
詳細はこちらをご参照ください。
github.com
プログラミング言語はなんでもいいのですが、Rubyのライブラリが一番開発されている(一番コミットされている)ようなのでRubyにしました。
Coincheck APIのRuby版ライブラリについてはこちらをご参照ください。
github.com
今回書いたソースコードにはaskやbidといった用語が出てきます。
それらの意味については以下の取引所APIドキュメントに説明があります。
coincheck.com
ソースコードおよびその説明
今回書いたソースコードは以下です(後述の説明のため、ファイル名はcoincheck_trader.rbとします)。
require 'ruby_coincheck_client' require 'yaml' class CoincheckTrader < CoincheckClient def initialize(api_key, secret_key, name, cash) super(api_key, secret_key) @name = name @cash = cash @holdings = [] @ask_prices = [] @bid_prices = [] end def trade(trade_mode = 'moving_average') case trade_mode when 'moving_average' then moving_average_trade end end def moving_average_trade number_transaction = ENV['NUMBER_TRANSACTION'].to_i interval_second = ENV['INTERVAL_SECOND'].to_i window_length = ENV['WINDOW_LENGTH'].to_i log_file_path = "log_nt_#{number_transaction}_is_#{interval_second}_wl_#{window_length}.txt" log_file_mode = 'w' log_file_perm = 0755 file = File.new(log_file_path, log_file_mode, log_file_perm) file.puts('[START]') file.puts("[trader name:#{@name}]") file.puts("[balance:#{get_balance}]") file.flush window_length.times { ask_price, bid_price = get_price('ask+bid') add_latest_price(ask_price, bid_price) sleep interval_second } number_transaction.times { |epoch| ask_price, bid_price = get_price('ask+bid') file.puts("[epoch:#{epoch}]") file.puts("[ask price:#{ask_price}][bid price:#{bid_price}]") file.puts("[balance:#{get_balance(bid_price)}]") file.flush if ask_price < @ask_prices.sum / window_length amount = @cash / ask_price if amount > 0 buy('BTC', ask_price, amount) file.puts("[buy][price:#{ask_price}][amount:#{amount}]") end next end @holdings.each { |holding| if @bid_price.sum / window_length < holding['price'] sell('BTC', bid_price, holding['price'], holding['amount']) file.puts("[sell][ask_price:#{holding['price']}][bid_price:#{bid_price}][amount:#{holding['amount']}]") end } delete_oldest_price add_latest_price(ask_price, bid_price) sleep interval_second } file.puts('[END]') file.puts("[trader name:#{@name}]") file.puts("[balance:#{get_balance(get_price('bid'))}]") file.flush end def buy(type, price, amount) @cash -= (price * amount) holding = {} holding['type'] = type holding['price'] = price holding['amount'] = amount @holdings << holding end def sell(type, bid_price, ask_price, amount) @cash += bid_price * amount holding = {} holding['type'] = type holding['price'] = ask_price holding['amount'] = amount @holdings.delete(holding) end def add_latest_price(ask_price, bid_price) @ask_prices << ask_price @bid_prices << bid_price end def delete_oldest_price @ask_prices.drop(1) @bid_prices.drop(1) end def get_balance(price = 0) balance = @cash @holdings.each { |holding| balance += price * holding['amount'] } return balance end def get_price(type) ticker = JSON.parse(read_ticker.body) case type when 'ask' then return ticker['ask'] when 'bid' then return ticker['bid'] when 'ask+bid' then return ticker['ask'], ticker['bid'] end end end config_file_name = 'config.yml' config = YAML.load_file(config_file_name) ct = CoincheckTrader.new(config['API_KEY'], config['SECRET_KEY'], config['TRADER_NAME'], config['JPY_CASH']) trade_mode = ENV['TRADE_MODE'] ct.trade(trade_mode)
処理の流れは以下のようになっています。
- 設定ファイルconfig.ymlからAPIキー、シークレットキー、トレーダー名、元金(円)を取得し設定
- シェル変数TRADE_MODEからトレードモードを取得し設定
- トレードモードがmoving_averageの場合はシェル変数から以下の値を取得し、設定
シェル変数 | 意味 |
---|---|
NUMBER_TRANSACTION | トランザクション数(売買の回数という意味です) |
INTERVAL_SECOND | 休憩時間の秒数(interval_second秒ごとに板情報からBTC/JPYの最新価格を取得します) |
WINDOW_LENGTH | 単純移動平均(以下、移動平均)における区間の長さ(ここにおける区間とはinterval_second秒ごとのタイミングの回数という意味です) |
4. 区間の長さだけBTC/JPYのask(買値)とbid(売値)を取得
5. トランザクション数だけ以下のことを行います。
- BTC/JPYのask(買値)とbid(売値)を取得
- 最新の買値が買値の移動平均値よりも安ければ、現金で買えるだけBTCを購入
- 買い注文をしてないときに最新の売値が売値の移動平均値よりも高ければ、保持してるBTCを売却
- 買値の配列ask_pricesから最も古い買値を、売値の配列bid_pricesから最も古い売値をそれぞれ削除
- 買値の配列ask_pricesに最新の買値を、売値の配列bid_pricesに最新の売値をそれぞれ追加
プログラムの実行方法
まずcoincheck_trader.rbと同一のディレクトリに以下のような内容のconfig.ymlを作成してください。API_KEYとSECRET_KEYには事前準備で取得した値を設定します。
API_KEY: XXXXXXXXXXXXXXXXX SECRET_KEY: YYYYYYYYYYYYYYYYYYYYYYYYY TRADER_NAME: ZZZZZZ JPY_CASH: 1_000_000
そして、ruby_coincheck_clientのGitHubのウェブページにも説明がありますが、Gemfileに
gem 'ruby_coincheck_client'
を追記した場合には以下を実行してください(※bundleをインストールする必要があります)。
bundle
もしくは、以下でも大丈夫です(※gemをインストールする必要があります)。
gem install ruby_coincheck_client
以下のようなコマンドでプログラムを実行することができます。
TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=8 ruby coincheck_trader.rb
実行結果および考察
プログラムの実行方法と同じ条件でプログラムを実行しました。
出力したログの一部を抜粋すると、以下のようになっていました。
[START] [balance:1000000] [END] [balance:1000077.6524122722]
つまり、元金100万円で3秒ごとの1000回の売買タイミングを与えると77円の利益が出ました!
最初に移動平均の値を取得するために秒、売買時間が秒、約50分で77円の利益が出たことになります。
本当は多数回実験して平均値と不確かさを出す方が良いのですが、今回は省略しました。
今回の方策におけるテーマは移動平均でしたが実は移動平均の区間の長さ(ソースコードで言う所のwindow_length)をどの長さにするのがいいのかを調べたいという思いもありました。
そこで、このようなシェルスクリプトを何回か実行しました。
#!/bin/bash TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=1 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=2 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=4 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=8 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=16 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=32 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=64 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=128 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=256 ruby coincheck_trader.rb & TRADE_MODE=moving_average NUMBER_TRANSACTION=1000 INTERVAL_SECOND=3 WINDOW_LENGTH=512 ruby coincheck_trader.rb &
結果、API制限をいただきました苦笑
同じ条件では同名のログファイルに上書きしてたのですが、ファイル名にタイムスタンプなどを含めて別名にしておくべきでした。
最新ファイルは途中までしかなくて、真相は闇の中です。
おわりに
今回はCoincheck APIを利用して、テクニカル分析で有名な移動平均に基づいた仮想トレードプログラムを作成しました。
そして、奇跡的に77円の利益が出ました。
きっとルールベースでもより利益が得られるトレードプログラムは作れると思います。
さらに深層学習や強化学習といった機械学習を利用する方法もあると思います。
それではまた。