エンジニアの頭の中

トレードbotエンジニアが書く技術と投資のブログ

自動トレードbotによる取引所API実行時の通信エラーと3つの対策

f:id:mitsu3204:20180928141946j:plain

自作したボットを運用して仮想通貨を自動でトレードしています。現在、取引所はbitFlyerとBitMEXの二つを使用しています。

今日は、ボット運用者なら誰でも経験しているであろう外部サーバへの通信処理の際のエラーについて書きます。

ネットワークを介した処理は失敗するもの

ボットを運用して自動トレードを行なっていると、取引所や自分が運用している別のサーバなど、ボットが稼働しているサーバの外へ通信する必要が頻繁に発生します。 
こういった外部との通信を伴う処理に失敗はつきものです。

ネットワーク越しの外部サーバへの通信が成功するかどうかは、こちらのプログラムが稼働する環境とは異なる外の世界での状況に依存するため、完全なコントロールできるものではなく、100%成功させることなど不可能です。

よって、自動トレードを行うボットに限った話ではなく、外部との通信を伴うプログラムには、必ず通信が失敗する可能性を考慮して実装する必要があるのです。

自動トレードを行うボットにおける外部通信処理というと、主に 取引所のAPIの呼び出し が該当すると思います。価格、出来高などの各種情報の取得、注文の発行、キャンセルなどの実行がこれにあたります。

自動トレードを行うボットの場合、数十秒や数分おきに一連の処理を実行しているものもあり、こういったボットは取引所のAPIの実行頻度も高く、エラーに出くわすことは頻繁にあります。

取引しているのは仮想通貨ですから、直接的にお金と関わってくるものです。 意図した通りに取引を実行できなければ、致命的な損失(機会損失も含む)につながる可能性があるため、エラーへの対処というのは非常に重要になってきます

API実行時の通信エラーの内容

クライアント側から取引所に対して、注文発行などの通信処理を実行した場合に、どのようなエラーがありえるでしょうか。いくつか考えられるものを記載します。

ボットを運用している人は皆、恐らくどれかしら出くわしたことがあると思います。

サーバとのコネクションが確立できずにタイムアウト

通信処理の最初の段階で、サーバとのコネクションを確立するため、クライアント側からサーバに対して接続要求を行ったものの、一定時間の間にサーバ側から応答を受け取れず、クライアント側でタイムアウトが発生するケースです。 いわゆるコネクションタイムアウトですね。

レスポンスを受け取れずタイムアウト

クライアント、サーバ間のコネクションは確立していて、クライアント側からリクエストの送信は行ったものの、そのリクエストに対するサーバからの応答を一定の時間が経過するまでに受け取れず、タイムアウトするケースです。リードタイムアウトです。

いきなり切断

何らかの原因で、リセットのパケットが投げられ(どこが投げているかは状況により様々)、接続が切断される場合もあります。エラー内容としてはコネクションリセットです。

500番台のHTTPステータスコードが返される

サーバ側に問題が発生している場合です。事象と原因は様々ですが、サーバ側のバグ(この場合は基本500を返してくる)でなければ、一時的にサーバや通信経路が混み合っているというケースが多いです。「しばらく待ってから接続してください」などのメッセージが返されます。

API実行時の通信エラーへの3つの対策

通信処理の失敗は、完全には避けられないものと書きましたが、ではどうやって対策したら良いのでしょう?完全な対策など不可能ですが、リスクを小さくするための対策はいくつかあります。

対策その1.脆弱なサービスを使用しない

そもそもですが、裁量だろうがボットだろうが、トレードに伴うリスクは少しでも小さくしておいたほうが良いと考えています。そのために、勝負する場やツールを選ぶことは重要です。

アクセスが集中した際にすぐにサーバがダウンしたり、遅延、エラー等が発生するような取引所や取引ツールは、選択肢から外しておいたほうが無難でしょう。

どんなに優秀な取引ルールを使っていても、売買シグナルの通りに取引を実行できないのでは意味がありません。

対策その2.通信回数は必要最小に抑える

各通信処理が失敗する可能性がゼロではない以上、通信エラーによるリスクを減らすためには、 通信処理を実行する回数は少しでも少ないほうが良いです

例えば、一回のAPI呼び出しでまとめて取得可能なものを、分割して取得している箇所などがあれば、一括して取得したほうが良いでしょう。

また、残高やポジションの情報などを、必要になった際に都度API呼び出しを行うのではなく、情報の鮮度的に問題無いのであれば、前回の取得分をメモリ上にキャッシュしておくのも良いです。

通信処理というのは、通信先のサーバや回線の状況によっては、レスポンスを受け取るまでに数秒かかってしまう場合もあります。高頻度取引ボットの場合は、数秒が致命的になる場合もあるでしょう。そのため、プログラムの実行速度の観点から見ても通信回数は少ないに越したことはないです。

頻繁にこちらからリクエストを送信する必要があるREST APIではなく、取引所がwebsocketを用意しているのであれば、websocketを使用するのも対策になると思います。

www.hacky.xyz

対策その3.失敗した通信をリトライする

失敗した通信処理をもう一度実行します。プログラムの実装として対処が必要になってくるのはこれです。

こちらのプログラムの不備によって、誤ったリクエストを送信した場合(400系のエラー)を除けば、通信処理の失敗原因というのは、一時的な外部要因によるものであることがほとんどです。こういった場合、一度失敗しても再度通信処理を実行すれば、2度目は成功する可能性があります。

ただし、通信処理をリトライする場合は、何も考えずリトライを繰り返せば良いというわけではありません。リトライを実装する際に考慮すべき点として以下があげられます。

  • 失敗した処理はリトライして問題が無い内容なのか?
  • リトライ対象とするエラーはなにか?
  • 何回までリトライを繰り返すか?
  • リトライの時間間隔はどの程度空けるか?
本当にリトライしても良いのか?

まず最初に、そもそもリトライをして問題が無いかどうかを考える必要があります。

例えば、特定の時間内に完了させる必要がある処理であった場合、通信の失敗によってリトライの実行タイミングは既に該当の時間外になってしまっているかもしれません。このような短い期間で実施しなくてはならない処理の通信失敗の対応としては、リトライは有効な手段にはならないでしょう。

実行した処理がデータの登録、変更、削除などを行ういわゆる更新系の処理の場合も注意が必要です。

というのも、一度目の通信処理がエラーとなっていても、通信先の’サーバでは正常に処理が受け付けられていた場合があり得るからです。この場合、リトライによって、同様のリクエストを再度送りつけることになってしまい、注文の二重送信などにつながる可能性があります。

わかりやすいのは、Read Time Outによって、エラーとなった場合です。これはクライアント側(こちらのプログラム)が、通信先であるサーバに対して通信処理の要求(APIの呼び出しなど)を行ったあと、サーバ側から一定時間レスポンスが返されないことにうよって起きるものです。

あくまでもクライアント側が、待ち時間を過ぎてもレスポンスを受け取れなかったというだけで、サーバ側では正常に処理が完了している場合があります。こういうことがありえるので、更新系の処理が、リードタイムアウトでエラーとなった場合はリトライを行うべきでは無いでしょう。

価格、残高、ポジションの取得などのように参照系の処理の場合は、再処理によるデメリットがないため、リードタイムアウトかどうかは気にせず、リトライしても問題ありません。

また、そもそもですが、更新系か参照系かに関わらず、クライアント側のプログラムの不備が原因と思われるエラー(400系)が発生した場合は、リトライしてはいけません。 よって、エラーが発生した場合は、その内容をチェックしてからリトライをするかどうかを決める必要があります。

何回までリトライを繰り返すか?

サーバに大量に処理要求を受け付けていて、混み合っている場合は、リトライしても再度エラーとなることがあります。

一般的には、リトライの回数に上限を設けて、上限まで試行し続けても通信が成功しなかった場合は、エラーとして異常終了させたり、諦めて後続の処理を行ったりします。

ただし、何回が適切かは決まりは無いですし、クライアント側の要件によっても異なると思います。

例えば、5分足のローソク足から得た売買シグナルに基づいて取引所のAPIを呼び出して注文して、リトライを繰り返した結果、売買シグナルの検知から5分以上経過してやっと発注できたといっても、古いシグナルに基づいて出された注文ということになってしまいます。

リトライの時間間隔はどの程度空けるか?

一度接続して通信エラーが返ってきているのだから、直後にもう一度接続しにいっても状況は変わらないかもしれません。

そこで、リトライする場合は、プログラムをスリープさせて多少の時間を空けてから実行するというのが一般的です。

リトライ間隔も、回数と同様で何秒が最適かの共通の解はありません。ただ、通常は「秒」単位で考えます。

リトライ間隔の決め方としては「Exponential Backoff」という良い方式があります。

これは、リトライ間隔を固定にするのではなく、実行するたびにその間隔を1秒、2秒、4秒、8秒というように指数関数的に増加させていくものです。(2乗でないといけないわけではないです)

以上になります。

対策の最後にあげているリトライに関しては、ライブラリに備わっている機能を使ってもよいですし、難しくはないため自前で実装しても良いと思います。

forやwhileによるループとその中で例外のハンドリング処理を書けば、すぐに実装できるでしょう。