HFTボットのアーキテクチャ_02

HFTボットのアーキテクチャ実例

まずはこのマガジンの第一弾として、最近着手していたHFTの実装アーキテクチャの話に触れたいと思います。
今回の記事は、既にHFTボットの開発経験がある人向けです。あるいは、これからボット開発に取り組もうとしている方にとっては有益な内容になると思います。
実装ではなく理論や仕組みにご興味のある方は、別の記事で取り上げようと思いますので、お待ちください。

前置き

僕たちが開発している暗号通貨用のHFTボットはPythonによって作られています。
なぜPythonか?というそもそも論ですが、
①アルゴや演算系のライブラリや文献が整っていて開発しやすい。
②処理速度のハンディキャップが現状であれば問題になりにくい(プログラムの処理速度<<API通信速度)。
ということで、メリットがデメリットを十分上回っているだろうと判断してのことです。

僕たちのHFTボットは、昨年の6月ごろにUKIが1万円チャレンジと称して低資金・高利回りを実現するために作ったものが長らく原型となっていました。

このチャレンジの後、しばらくは稼働させていなかったのですが、昨年9月中旬ごろからBTC市場のボラティリティが急速に無くなってしまいました。この時期、主戦力として運用していたスイングボットたちの収益力が落ちてしまったので、それらに安定して資金を供給するために、10月ごろから本腰を入れてHFTの収益化に取り組み始めました。

その際、元々のプログラムをベースに開発をしたため、本来あるべきアーキテクチャをないがしろにした状態でロジック周りを中心に改善を進めてきました。
今年1月まではロジック周りで様々な試行錯誤を繰り返し、今後の方向性がある程度固まりましたので、このあたりで土台整備に立ち戻ろうと決意した次第でありました。

改修前のアーキテクチャ

まず、改修以前のアーキテクチャを紹介し、この構造における問題点について説明します。

(図1. 改修前のアーキテクチャ)

API通信を多用するというボットの特性上、スレッドが多用されていますが、特に変哲のない構造です。特徴として、

・1プロセス内で完結している。
・Websocketを二重接続しているのだが、on_message内で板の構築とアップデートを行っている(ここが構造的にNG)。
・それぞれの接続に応じて板やティッカーも二重に保有している(無駄)。
・監視スレッドが遅延状況を判断し、どちらの板を使うべきか切り替えている。また、遅延が大きくなった場合、Websocketの再接続を促す。
・発注処理はスレッドプールに任せている。

などが挙げられます。

この構造では、以下のような問題が発生します。

①Websocketメッセージの処理遅延
Websocketからメッセージを受信したそのタイミングで、メモリ上に構築した板のアップデートを行っています。
ここでの処理や同プロセス内の他の処理が重くなり、「メッセージの受信間隔<メッセージの処理時間」になってしまうと、Websocketからのメッセージが抜け落ちてしまいます。
普段、サーバ側の配信遅延に文句を言っている僕たちにとって、自前のシステムのせいでメッセージが抜け落ちる可能性があるのは最悪です。

※ちなみに、この板の構築とアップデートの処理が、Websocketからのデータをハンドリングする上で処理性能上のボトルネックになりがちです。
利便性の高いインメモリDBなどを使う実装方法もありますが、僕は単純な配列構造でデータを保持し、価格と配列のIndexの紐づけを行う、というやり方にしています。別noteで紹介しますので、フィードバックをいただけたらと思います。

②システム・リソースを活かしきれない
Python(多くの人が使っているC言語実装のCPython)はGILというロックの仕組みを採用しており、メモリ空間を共有するシングルプロセス&マルチスレッドの構造では、特にマルチコアCPUなどのシステムリソースを全く活かしきれません。
(もちろんボット内ではAPIリクエストを多用しますので、その部分の非同期化は最低限必要なことではありますが)


さて、これらの問題を解決していくわけですが、Twitter上には有難いヒントを発信しておられる人たちが存在します。
僕は元々のエンジニア職から離れて既に7~8年経とうとしておりますし、通信系のシステムを業務レベルで実装したことはありませんので、大いに参考にさせていただきました。

改修後のアーキテクチャ

以下が、アーキテクチャの設計指針です。

・Websocketの接続部分はプロセスを分け、余っているシステムリソースを活用すると同時に、メインプロセス上の重い処理などの影響を受けにくくする。
・Websocketの接続部分の応答性能を良くするため、またプロセス同士の間をなるべく疎結合にするため、Twitterのアドバイス通り、キューにメッセージを突っ込むだけにする。
・ただし、板構築の余計な処理を省けるよう、冗長なメッセージはあらかじめキューに突っ込む前にフィルタしておく。

こうして、現状は以下の構造となっています。

(図2. 改修後のアーキテクチャ)

プロセスは、

①メインプロセス
②Websocket用プロセス(複数立ち上げる場合あり)
③ログ用プロセス

の3つ(+α)です。

②のWebsocket用プロセスでは、冗長張ったコネクションから流れてくるメッセージをフィルタリングし、マルチプロセス用のキュー(multiprocessing.Queue)にPutします。この時点で、遅い方のメッセージは読み捨てられます。

一方の①のメインプロセス側では、キューの個数の分だけ待ち受け用のスレッドを作成し、このスレッドたちでインメモリの板やティッカー情報を更新します。この機能群を「MessageHandler」インターフェースとしてまとめ、各取引所のメッセージフォーマットごとに実装しています。

ロジックスレッドではWebsocketの通信を意識せず、Handlerクラスが保持する板やティッカーを参照するだけです。

また、監視用のスレッドでは、各プロセスの生存状況や、Websocket通信の遅延状況をモニタリングし、必要に応じてシステムストップを行います。

最後に③のログ用プロセスですが、これはマルチプロセスから単一のログファイルにアクセスする際のお作法です(詳細はドキュメントを参照)。


今回のnoteはここまでです。
WebsocketやAPIの断片的なコードはWeb上に散見するものの、HFTボットのアプリケーション的なアーキテクチャについて触れられている記事がなかったため、書いてみました。

もちろん、これがベストなアーキテクチャと言っているわけではなく、むしろより洗練するためのフィードバックを頂きたく思っています。
ご意見・ご感想、またご質問はTwitter @i_love_profit までお願いします。

この記事が気に入ったらサポートをしてみませんか?