2019年の元旦に、以下記事で目標に上げていた項目の一つの、仮想通貨の自動裁定取引システムの簡易バージョンが完成したので、1晩動かしてみました。その所感をまとめてみようと思います。正直、大きな課題が残っていますが、ひとまず現状をまとめてみました。資産形成の王道とは一味違った、面白さを感じ取って頂けたら幸いです。
裁定取引とは
裁定取引とは、理論上、100%利益が出る取引のことを言います。この説明は非常にうさんくさいですね(笑)少し具体的に説明します。仮想通貨のビットコインを例にとって説明します。
同じものが同時に別の場所で異なる価格で売買されているという市場の歪みを利用する取引
単一の仮想通貨取引所での買値と売値の関係は基本的に決まっているが…
仮想通貨のビットコインは、仮想通貨取引所で取引されています。取引所では、板形式の売買となり、参加者が値付けをして、売買がなされているわけです。売買では、売りたい人はより高い値段で売りたいですし、買いたい人はより安い値段で買いたいので、売買注文は以下のように売値と買値は乖離するのが自然です。当然、同じ取引所の中で、ビットコインを買って、同時にビットコインを売っても、売値と買値の乖離分、損するだけなので、意味はありません。
複数の取引所間では状況が変わってくる…
しかし、複数の取引所を視野にいれるとどうでしょうか。どの取引所も同じビットコインが売買されており、本質的には差は無いはずなのですが、双方の取引所での取引価格には、それなりに大きな差異が生じています。
あるタイミングの価格を具体的に見てみましょう。以下の結果をご覧頂きたいのですが、zaifという名前の取引所のベストaskよりもbitbankという名前の取引所のベストbidの方が高くなっていますね。これは、この瞬間に限り、zaifでビットコインを買って、bitbankでビットコインを同量売ることで、差額分の利益を出すことができることを意味します。
このように、(その瞬間に限り、)確実に利益を出すことができる状況を利用して、差益を確保するような取引を裁定取引と呼びます。
best ask rate by maker [ { name: 'zaif', ask: 409900, bid: 409760 }, { name: 'liquid', ask: 410471.28999, bid: 410350.26 }, { name: 'bitbank', ask: 411292, bid: 411292 } ] best bid rate by maker [ { name: 'bitbank', ask: 411292, bid: 411292 }, { name: 'liquid', ask: 410471.28999, bid: 410350.26 }, { name: 'zaif', ask: 409900, bid: 409760 } ] arbitrage [ { buy: 'zaif', ask: 409900, sell: 'bitbank', bid: 411292, arbitrage_profit: 1.3920000000000001 }, { buy: 'liquid', ask: 410471.28999, sell: 'bitbank', bid: 411292, arbitrage_profit: 0.8207100099999807 } ]
裁定取引のメリット
未来予測不要で機械的に利益を着実に出せる手堅い手法
取引が約定さえすれば、利益が確実に出るため、トレードの手法としては非常に手堅いといえるでしょう。さらに、取引すべき条件は、ある取引所のベストbid > 別の取引所のベストaskという(、未来予測しないという点で、)シンプルな条件となるので、簡単にロジックを組んで自動的にトレードを行うことができるというメリットもあります。
裁定取引のデメリット
約定しない場合の損失の可能性
さりげなく書いてある、「取引が約定さえすれば」という部分が、裁定取引の難しいところです。ある瞬間、裁定取引が成立する環境だったとしても、1秒後も確実にその取引が成立するかは、保証がありません。取引ペアの内どちらかが約定しないと、損失になってしまう可能性があります。
得られる利益が少なく手数料負けしやすい
基本的に、市場のほんのわずかな歪につけこむような取引手法なので、大きな利益は稼げません。本当に本当に小さな利益を、コツコツと積み上げていくようなそんな取引になります。利益があまりにも小さいので、規模が伴わないと、ちょっとしたことで発生する手数料であっという間に利益を失ってしまうことになります。
自動裁定取引ツールの簡易バージョンについて
現段階では、システムには課題が多いため、そのまま稼働させることはあまりオススメできませんが、一旦現状を公開しようと思います。
システム概要
Node.jsを利用し、npmパッケージccxtを利用し、Zaif, bitbank, Liquidの3つの取引所のAPIを利用し、各取引所での価格差と残高を調べ、裁定取引可能なら、各取引所に売買注文を出すという仕組みです。
設定手順
Node.jsインストール
Node.jsをインストールしてください。公式サイトから、LTS版をインストールするという感じで良いと思います。
作業フォルダ作成
適当なフォルダを作ります。ここでは仮にa2という名前のフォルダにします。(Automatic Arbitragerの略をイメージ。)
コマンドプロンプトで作業フォルダへ移動
上のフォルダへ移動するコマンド
cd ..
移動したいフォルダへ移るコマンド
cd 移動したいフォルダ名
等を組み合わせて該当フォルダへ移動します。
npm init
以下コマンドを実行し、出てくる内容に全てエンターで対応します。
npm init
ccxtのインストール
以下コマンドを実行しccxtをインストールします。
npm install ccxt --save
config.jsonファイルの作成
適当なテキストエディタでconfig.jsonという名前のファイルを作成し、以下をコピペし、PUT_YOUR_API_KEY_HERE、PUT_YOUR_API_SECRET_HEREを、各自、自分で設定したAPI情報に置き換えてください。正直、自動積立ツールと同じ設定ファイルを流用しており、いらない設定がいっぱい入っている&プログラム本体に設定項目が多く残ってしまっているのですが、あまり気にしないでもらえると助かります…)
{ "zaif": { "api": { "key": "PUT_YOUR_API_KEY_HERE", "secret": "PUT_YOUR_API_SECRET_HERE" }, "fee": { "btc": { "maker": 0, "taker": 0 }, "eth": { "maker": 0, "taker": 0.1 }, "xem": { "maker": 0, "taker": 0.1 }, "mona": { "maker": 0, "taker": 0.1 }, "bch": { "maker": 0, "taker": 0.3 }, "cms_xem": { "maker": 0, "taker": 0.1 }, "cms_eth": { "maker": 0, "taker": 0.1 } }, "tsumitate": { "per_year": 360000, "ratio": { "btc": 0, "eth": 33, "xem": 34, "mona": 0, "bch": 0, "cms_xem": 16.5, "cms_eth": 16.5 }, "interval": 86400000 } }, "liquid": { "api": { "key": "PUT_YOUR_API_KEY_HERE", "secret": "PUT_YOUR_API_SECRET_HERE" }, "fee": { "btc": { "maker": 0, "taker": 0 }, "eth": { "maker": 0, "taker": 0 }, "xrp": { "maker": 0, "taker": 0 }, "bch": { "maker": 0, "taker": 0 }, "qash": { "maker": 0, "taker": 0 } }, "tsumitate": { "per_year": 120000, "ratio": { "btc": 0, "eth": 0, "xrp": 0, "bch": 0, "qash": 100 }, "interval": 86400000 } }, "bitbank": { "api": { "key": "PUT_YOUR_API_KEY_HERE", "secret": "PUT_YOUR_API_SECRET_HERE" }, "fee": { "btc": { "maker": -0.05, "taker": 0.15 }, "xrp": { "maker": -0.05, "taker": 0.15 }, "mona": { "maker": -0.05, "taker": 0.15 }, "bch": { "maker": -0.05, "taker": 0.15 } }, "tsumitate": { "per_year": 240000, "ratio": { "btc": 25, "xrp": 25, "mona": 25, "bch": 25 }, "interval": 86400000 } } }
a2.jsファイルの作成
適当なテキストエディタでa2.jsという名前のファイルを作成し、以下をコピペしてください。実は設定項目が完全にconfig.jsonに分離できておらず、まだこちらに残っています。
裁定取引単位として0.001BTCを設定している箇所は「const arbitrage_trade_unit = 0.001」、実行間隔として10秒を設定している箇所は「const interval = 10000」、一回当たりの裁定取引での最低実現利益0.5円以上を設定している箇所は「//各ペア毎に順番に裁定取引実行時の損益を計算する var arbitrage_profit var arbitrage_profit_all = [] for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { arbitrage_profit = (ticker_maker_all_sell[j].bid – ticker_maker_all_buy[i].ask) * arbitrage_trade_unit if (arbitrage_profit > 0.5) { arbitrage_profit_all.push({‘buy’: ticker_maker_all_buy[i].name, ‘ask’: ticker_maker_all_buy[i].ask, ‘sell’: ticker_maker_all_sell[j].name,’bid’: ticker_maker_all_sell[j].bid , ‘arbitrage_profit’: arbitrage_profit}) } else { } } }」の赤字の箇所になります。これらは、実際にご使用になる皆様の希望にあわせて調整して頂くのが良いかと思います。
'use strict'; const ccxt = require ('ccxt'); //定義(設定) var fs = require('fs'); var config = JSON.parse(fs.readFileSync('./config.json', 'utf8')) //設定情報をjsonとして読込 //定義(ccxtにAPI情報を設定) let liquid = new ccxt.liquid ({ apiKey: config.liquid.api.key, secret: config.liquid.api.secret, }) let bitbank = new ccxt.bitbank ({ apiKey: config.bitbank.api.key, secret: config.bitbank.api.secret, }) let zaif = new ccxt.zaif ({ apiKey: config.zaif.api.key, secret: config.zaif.api.secret, }) //裁定取引単位 const arbitrage_trade_unit = 0.001 //実行間隔をミリ秒で設定する 例えば10秒なら 10 * 1000 const interval = 10000 const sleep = (timer) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, timer) }) } (async function () { while (true) { //日時取得、表示 const today_gmt = new Date() const today_jst = today_gmt + 9 * 60 * 60 * 1000 console.log(today_jst) //板情報取得 const ticker_all_exchanges = await Promise.all ([ liquid.fetchTicker ('BTC/JPY'), bitbank.fetchTicker ('BTC/JPY'), zaif.fetchTicker ('BTC/JPY'), ]) //各取引所の板情報取得 const ticker_btc_liquid = ticker_all_exchanges[0] const ticker_btc_bitbank = ticker_all_exchanges[1] const ticker_btc_zaif = ticker_all_exchanges[2] //ask値,bid値を入力した配列を作成(成行注文的価格一覧の配列) var ticker_all = [ {'name': 'liquid', 'ask': ticker_btc_liquid.ask, 'bid': ticker_btc_liquid.bid}, {'name': 'bitbank', 'ask': ticker_btc_bitbank.ask, 'bid': ticker_btc_bitbank.bid}, {'name': 'zaif', 'ask': ticker_btc_zaif.ask, 'bid': ticker_btc_zaif.bid}, ] //maker注文のための価格情報の配列を作成(bitbank,zaifのみベストレートに価格最小刻み幅分変更) var ticker_maker_all = [ {'name': 'liquid', 'ask': ticker_btc_liquid.ask, 'bid': ticker_btc_liquid.bid}, {'name': 'bitbank', 'ask': ticker_btc_bitbank.ask - 1, 'bid': ticker_btc_bitbank.bid + 1}, {'name': 'zaif', 'ask': ticker_btc_zaif.ask - 5, 'bid': ticker_btc_zaif.bid + 5}, ] //配列をask値で昇順にソート(買付価格が安いものから順に並べる) ticker_maker_all.sort (function(a, b){ if (a.ask < b.ask) return -1; if (a.ask > b.ask) return 1; return0; }) var ticker_maker_all_buy = ticker_maker_all console.log('best ask rate by maker') console.log(ticker_maker_all_buy) //配列をbid値で降順にソート(売り価格が高いものから順に並べる) ticker_maker_all.sort (function(a, b){ if (a.bid > b.bid) return -1; if (a.bid < b.bid) return 1; return0; }) var ticker_maker_all_sell = ticker_maker_all console.log('best bid rate by maker') console.log(ticker_maker_all_sell) console.log('arbitrage') //各ペア毎に順番に裁定取引実行時の損益を計算する var arbitrage_profit var arbitrage_profit_all = [] for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { arbitrage_profit = (ticker_maker_all_sell[j].bid - ticker_maker_all_buy[i].ask) * arbitrage_trade_unit if (arbitrage_profit > 0.5) { arbitrage_profit_all.push({'buy': ticker_maker_all_buy[i].name, 'ask': ticker_maker_all_buy[i].ask, 'sell': ticker_maker_all_sell[j].name,'bid': ticker_maker_all_sell[j].bid , 'arbitrage_profit': arbitrage_profit}) } else { } } } //裁定取引実行時の損益計算の配列を降順にソート(利益が大きいものから順に並べる) if (arbitrage_profit_all == []) { } else { arbitrage_profit_all.sort (function(a, b){ if (a.arbitrage_profit > b.arbitrage_profit) return -1; if (a.arbitrage_profit < b.arbitrage_profit) return 1; return0; }) //裁定取引実行時の損益表示 console.log(arbitrage_profit_all) } //残高取得 const balance_all_exchanges = await Promise.all ([ liquid.fetchBalance (), bitbank.fetchBalance (), zaif.fetchBalance (), ]) const balance_liquid = balance_all_exchanges[0] const balance_bitbank = balance_all_exchanges[1] const balance_zaif = balance_all_exchanges[2] console.log('liquid', 'BTC:',balance_liquid.BTC.free, 'JPY:',balance_liquid.JPY.free) console.log('bitbank', 'BTC:',balance_bitbank.BTC.free, 'JPY:',balance_bitbank.JPY.free) console.log('zaif', 'BTC:',balance_zaif.BTC.free, 'JPY:',balance_zaif.JPY.free) //買い,売り可不可チェック、取引所セット、発注済フラグ定義 var buy_flag = null var buy_exchange = null var sell_flag = null var sell_exchange = null var order_status = null //アービトラージ可能なら発注 if (arbitrage_profit_all.length > 0) { for (var k = 0; k < arbitrage_profit_all.length && order_status == null; k++) { //フラグリセット var buy_flag = null var buy_exchange = null var sell_flag = null var sell_exchange = null //買い側の残高チェック switch (arbitrage_profit_all[k].buy) { case 'zaif': buy_exchange = 'zaif' if (balance_zaif.JPY.free > (0.001 * ticker_btc_zaif.ask - 5)) { buy_flag = 'ok' } break case 'liquid': buy_exchange = 'liquid' if (balance_liquid.JPY.free > 0.001 * ticker_btc_liquid.ask) { buy_flag = 'ok' } break case 'bitbank': buy_exchange = 'bitbank' if (balance_bitbank.JPY.free > (0.001 * ticker_btc_bitbank.ask -1)) { buy_flag = 'ok' } break } console.log('買い検討', buy_exchange) console.log('残高チェック', buy_flag) //売り側の残高チェック switch (arbitrage_profit_all[k].sell) { case 'zaif': sell_exchange = 'zaif' if (balance_zaif.BTC.free > 0.001) { sell_flag = 'ok' } break case 'liquid': sell_exchange = 'liquid' if (balance_liquid.BTC.free > 0.001) { sell_flag = 'ok' } break case 'bitbank': sell_exchange = 'bitbank' if (balance_bitbank.BTC.free > 0.001) { sell_flag = 'ok' } break } console.log('売り検討', sell_exchange) console.log('残高チェック', sell_flag) //売買flagがどちらもokなら売買注文を出してforループを抜ける if (buy_flag == 'ok' && sell_flag == 'ok') { //買い switch (arbitrage_profit_all[k].buy) { case 'zaif': console.log('zaifで買い') console.log(await zaif.createLimitBuyOrder ('BTC/JPY', 0.001, ticker_btc_zaif.ask - 5)) break case 'liquid': console.log('liquidで買い') console.log(await liquid.createLimitBuyOrder ('BTC/JPY', 0.001, ticker_btc_liquid.ask)) break case 'bitbank': console.log('bitbankで買い') console.log(await bitbank.createLimitBuyOrder ('BTC/JPY', 0.001, ticker_btc_bitbank.ask-1)) break } //売り switch (arbitrage_profit_all[k].sell) { case 'zaif': console.log('zaifで売り') console.log(await zaif.createLimitSellOrder ('BTC/JPY', 0.001, ticker_btc_zaif.bid + 5)) break case 'liquid': console.log('liquidで売り') console.log(await liquid.createLimitSellOrder ('BTC/JPY', 0.001, ticker_btc_liquid.bid)) break case 'bitbank': console.log('bitbankで売り') console.log(await bitbank.createLimitSellOrder ('BTC/JPY', 0.001, ticker_btc_bitbank.bid + 1)) break } order_status = 'done' } } } //実行間隔だけ待機 await sleep(interval) } }) ();
起動手順
コマンドプロンプトで該当フォルダへ移動
a2.jsファイルが置いてあるフォルダへコマンドプロンプトで移動してください。
コマンドプロンプトでa2.jsを実行
コマンドプロンプト上で以下コマンドを実行すると、自動で裁定取引可能かどうかをチェックし、裁定取引可能な価格、残高であれば自動的に売買注文を出してくれるツールが動き出します。何もしないとずっと動き続けるので、終了したいときは、コマンドプロンプト上で「Ctrl」+「C」をキーボード入力すると終了できます。
node a2.js
結果、所感等
損益
10秒に1回裁定取引機会を伺い、1回の裁定取引で1円以上の利益が出る場合のみ、裁定取引を行う設定で、1晩で76回の裁定取引が実行され、約90円の利益が出ました。売買代金は約6万円だったので投下資本に対する利益率は0.15%/日だったという状況です。
所感
思ったよりは利益が出たという印象だが…
思ったよりは利益が出たというのが正直な印象なのですが、大きな課題があり、そのままでは使い物にならないな…と今のところ感じています。
取引所の価格差は一方的な傾向が続いているように見える
理論的には、取引所間の価格差は、一時的に広がっても、もとに戻っていったり、逆の価格差が生まれたりするのでは…と思っていました。しかし、テスト稼働している間は、ほぼ100%、Zaifでの価格が安く、bitbankかLiquidの価格が高いという状況が続いていました。
結果として、Zaifで買って、それ以外で売るというパターンしか裁定取引できなかったため、ZaifのJPYとbitbank、LiquidのBTCが早々に枯渇し、以降は裁定取引したくても残高が足りないという状況になってしまいました。おそらく、裁定取引の機会はもっとあったと思うので、やり方次第では、もっと大きな利益も狙えるのではないか…という気もします。
リバランス取引の実装が必要
結論としては、利益を取りに行く裁定取引と、損失覚悟で残高バランスを戻すリバランス取引の両方を組み合わせる必要がありそうです。利益を取りに行く裁定取引の利幅は大きめに、損失覚悟で残高バランスを戻すリバランス取引の損失幅は小さめに設定することで、継続的に小さな利益を積み上げ続ける仕組みができるのではないかな…と期待していますが、実際のところはどうなのでしょうか。
まとめ
結構面白そうな結果が出て、意外な気持ちです。(正直、もっと、損益厳しかったり、裁定機会が捉えられなかったり等、無理ゲーな感じかと思っていましたが意外…。)残高リバランスの機能が実装できて、継続的に裁定取引可能な仕組みが出来上がったら、またご紹介させて頂ければ…と思います。ご興味あれば、色々お付き合い頂けますと大変ありがたいです。例によって、バグ報告、改善提案等も大歓迎です。今後ともよろしくお願いします。