Coincheck API を利用した仮想トレードプログラム

f:id:xgxgh:20200624110402p:plain

みなさん、こんにちわ。
マネックス証券システム開発部の平井です。
最近は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)

処理の流れは以下のようになっています。

  1. 設定ファイルconfig.ymlからAPIキー、シークレットキー、トレーダー名、元金(円)を取得し設定
  2. シェル変数TRADE_MODEからトレードモードを取得し設定
  3. トレードモードが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円の利益が出ました!
最初に移動平均の値を取得するために8*3=24秒、売買時間が1000*3=3000秒、約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円の利益が出ました。
きっとルールベースでもより利益が得られるトレードプログラムは作れると思います。
さらに深層学習や強化学習といった機械学習を利用する方法もあると思います。
それではまた。