エンジニアの頭の中

知識と技術で「お金稼ぎの自動化」を実現するため、日々奮闘するエンジニアのブログです。

【bitflyer bot開発】pybitflyerのAPI実行時間が遅い問題の対策

f:id:mitsu3204:20190131131951j:plain

目次

  • はじめに
  • pybitflyerの実行時間
  • ccxtの実行時間
  • pybitflyerの実行時間が遅い原因
  • 対策

はじめに

pybitflyerを使ってbitFlyerAPIの呼び出しには、実行時間が遅いという注意点があります。

ボット開発をある程度やっている人であれば、この問題は知っているとは思うのですが、参入したばかりの初心者のかたは知らないと思いますので、この点について遅さの程度や原因、対策などを記載します。

pybitflyerの実行時間

以下のコードで、pybitflyerを使用してAPIを呼び出した場合の実行時間を測ってみます。

エンドポイントは/v1/me/getpositions(ポジション情報を返すPrivate API)、パラメータとして、product_code='FX_BTC_JPY'を指定しています。 ※エンドポイントはなんでもよい

forループでAPI呼び出しを5回実行してみます。各呼び出しの前後ではtime.time()を呼び出すことによって、現在時刻を取得し、それらの差分を実行に要した時間(秒)としてprint関数で出力しています。

import time
import pybitflyer

client = pybitflyer.API(api_key='xxxxx', api_secret='xxxxx')

for i in range(5):
    start = time.time()
    client.getpositions(product_code='FX_BTC_JPY')
    print(time.time() - start)

実行結果は以下のとおりとなりました。

0.30104780197143555
0.2844119071960449
0.2470989227294922
0.2796781063079834
0.2731478214263916

多少のブレはあるものの、各実行ごとに大体同程度の時間(0.24秒〜0.3秒)がかかっていることがわかります。

ccxtの実行時間

次にAPIの呼び出し部分をccxtに変更して、同じエンドポイントを複数回呼び出した場合の実行時間を見てみます。

ccxtについての説明は省略します。興味のあるかたは過去記事をご覧ください。

www.hacky.xyz

import time
import ccxt

client = ccxt.bitflyer({'apiKey': 'xxxxx', 'secret': 'xxxxx'})

for i in range(5):
    start = time.time()
    client.fetch2(path='getpositions', api='private', params={'product_code': product_code})
    print(time.time() - start)

実行結果

0.2545909881591797
0.09581303596496582
0.08671736717224121
0.0951840877532959
0.07862710952758789

1回目の実行時間は、pybitflyerと同程度ですが、2回目以降の実行時間が明らかに短くなっているのがわかります。

pybitflyerの実行時間が遅い原因

pybitflyerもccxtも内部では同じ/v1/me/getpositionsのエンドポイントに同じパラメータでアクセスしているのになぜ実行時間が異なるのでしょうか?

これは、ログを見てみるとわかりやすいです。先ほどのコードの実行時の詳細な挙動を確認するため、以下の二行をAPIを実行する行より前の行に追加します。

import logging
logging.basicConfig(level=logging.DEBUG)

上記のコードを加えたうえで、先ほどのコードをそれぞれ実行すると、出力内容は以下のようになります。

pybitflyer
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
0.2870159149169922
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
0.27445411682128906
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
0.6675050258636475
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
0.3120310306549072
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
0.27839088439941406
ccxt
DEBUG:ccxt.base.exchange:GET https://api.bitflyer.jp/v1/me/getpositions?product_code=FX_BTC_JPY, Request: {'ACCESS-KEY': 'XGqNMAEXAXUSa8Lin7Jw4', 'ACCESS-TIMESTAMP': '1548905426', 'ACCESS-SIGN': '65766dee857b3bc2910a4a069713765b6d1fdbdfabae3c34319d0736bff5b1b7', 'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.19.1', 'Accept-Encoding': 'gzip, deflate'} None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443
0.2656219005584717
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
DEBUG:ccxt.base.exchange:GET https://api.bitflyer.jp/v1/me/getpositions?product_code=FX_BTC_JPY, Response: 200 {'Date': 'Thu, 31 Jan 2019 03:30:26 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '2', 'Connection': 'keep-alive', 'Set-Cookie': '__cfduid=dcbfd890e4a5cfa3ade304ebde912ee6f1548905426; expires=Fri, 31-Jan-20 03:30:26 GMT; path=/; domain=.bitflyer.jp; HttpOnly; Secure', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '-1', 'Request-Context': 'appId=cid-v1:e4fbc941-a2df-48ac-bbac-f2180e904002', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'X-Frame-Options': 'sameorigin', 'Content-Security-Policy': "default-src http: https: ws: wss: data: 'unsafe-inline' 'unsafe-eval'", 'Strict-Transport-Security': 'max-age=31536000', 'Expect-CT': 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', 'Server': 'cloudflare', 'CF-RAY': '4a191982d86ea55a-NRT'} []
DEBUG:ccxt.base.exchange:GET https://api.bitflyer.jp/v1/me/getpositions?product_code=FX_BTC_JPY, Request: {'ACCESS-KEY': 'XGqNMAEXAXUSa8Lin7Jw4', 'ACCESS-TIMESTAMP': '1548905426', 'ACCESS-SIGN': '65766dee857b3bc2910a4a069713765b6d1fdbdfabae3c34319d0736bff5b1b7', 'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.19.1', 'Accept-Encoding': 'gzip, deflate'} None
0.08129477500915527
DEBUG:urllib3.connectionpool:https://api.bitflyer.jp:443 "GET /v1/me/getpositions?product_code=FX_BTC_JPY HTTP/1.1" 200 2
DEBUG:ccxt.base.exchange:GET https://api.bitflyer.jp/v1/me/getpositions?product_code=FX_BTC_JPY, Response: 200 {'Date': 'Thu, 31 Jan 2019 03:30:26 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '2', 'Connection': 'keep-alive', 'Set-Cookie': '__cfduid=dcbfd890e4a5cfa3ade304ebde912ee6f1548905426; expires=Fri, 31-Jan-20 03:30:26 GMT; path=/; domain=.bitflyer.jp; HttpOnly; Secure', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '-1', 'Request-Context': 'appId=cid-v1:e4fbc941-a2df-48ac-bbac-f2180e904002', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'X-Frame-Options': 'sameorigin', 'Content-Security-Policy': "default-src http: https: ws: wss: data: 'unsafe-inline' 'unsafe-eval'", 'Strict-Transport-Security': 'max-age=31536000', 'Expect-CT': 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', 'Server': 'cloudflare', 'CF-RAY': '4a19198358e9a55a-NRT'} []
DEBUG:ccxt.base.exchange:GET https://api.bitflyer.jp/v1/me/getpositions?product_code=FX_BTC_JPY, Request: {'ACCESS-KEY': 'XGqNMAEXAXUSa8Lin7Jw4', 'ACCESS-TIMESTAMP': '1548905426', 'ACCESS-SIGN': '65766dee857b3bc2910a4a069713765b6d1fdbdfabae3c34319d0736bff5b1b7', 'Content-Type': 'application/json', 'User-Agent': 'python-requests/2.19.1', 'Accept-Encoding': 'gzip, deflate'} None

・・・省略・・・

ccxtのほうはログの出力量が多いため、3回目以降の実行分は省略しました。

それぞれのログの中には、

DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.bitflyer.jp:443

という出力があります。

この1行は「api.bitflyer.jpのサーバの443ポートに対して、HTTPSで接続します」という意味です。

この接続確立を示すログが、pybitflyerではAPIを呼び出すたびに出力されているのに対して、ccxtでは最初の呼び出しの時のみこの1行が出力されており、2回以降の呼び出しでは、同様の出力はないことがわかります。

つまり、pybitflyerでは、APIを実行するための関数が呼び出されるたびに、新しい接続(コネクション)を確立しているということです。これがpybitflyerの実行時間がccxtと比較して遅い原因となっています。

接続の確立というのは、いくつかの手続きを行う必要があるため、確立した接続を使用するだけの場合に比べて、処理時間がかかるものです。

ccxtでpybitflyerと同じ事象が起きないのは、ccxtが内部で一度作成した接続を破棄せず利用し続ける実装からです。

対策

接続を使い回すかどうかによる実行時間の差は、実行ごとにせいぜい数百ミリ秒程度なので、ボットの取引の実行頻度が、分単位やそれより長い場合であれば、特に問題とはならないでしょう。

高頻度で取引を行うボットの場合は、数十や数百ミリ秒の遅れは無視できないものとなると思います。

よって、対策としては、ccxtのように確立した接続を保持するライブラリを使用するか、pybitflyerを改良して使用するのが良いと思います。ただし、ccxtはセッションは使い回しているものの、呼び出す関数によってはAPIの実行回数自体が多くなってしまう場合があるので、この点は注意が必要です。

pybitflyerを改良するのであれば、既にpybitflyerのリポジトリに対する以下のPR(作られてから結構経っているがマージされていない)が、まさにセッション保持の機能追加を提案しているので、これを使えば良いと思います。

github.com