Rustで作る!Market Maker Bot (mmbot)
TL;DR
- Rust言語でMarket Maker Bot向けフレームワークを設計・実装してみた
- 取引戦略と取引所APIクライアントの実装例も作成した
- 実際に取引所テストネットに接続して動作確認を実施した
はじめに
こんにちは。magitoと申します。
筆者は普段、DeFi/DEXのドメインでTrading Bot[1]を開発・運用しています。Botの実装言語としては、以前はPythonやTypeScript/node.jsを使用することが多かったのですが、最近はRustにほぼ一本化しています。主な理由は以下です。
- 実行パフォーマンス:取引執行に高速性が要求されるオンチェーン戦略[2]を実装するのに適している
- 高生産性:大規模なシステムでも効率的かつ安全に開発を進めることができる
- ブロックチェーンとの相性:Rustで実装されているチェーンの場合、Botとブロックチェーンノードの統合が容易であったり、Bot開発にも直接利用できるSDKやライブラリが豊富である
上記の1,2については従来のトレーディングドメイン(主にMarket Makingなどの高頻度取引)においても魅力的な言語の特徴だと思っているのですが、ネットをざっと調べた限りでは「RustによるBot開発」というトピックに関する情報は(PythonやJSなどの他言語に比べると)極めて少ないように感じました。
そこで、今回は自分で実際にRustを用いてMarket Maker Bot向けフレームワークを設計・実装してみることにしました。
ソースコードは以下のリポジトリに置いてあります。
本稿では作成したシステムについて紹介しますが、あまり設計や実装の詳細に踏み込みすぎるとドキュメントのようになってしまい読む側も書く側も大変ですので、主要な部分をピックアップする程度にとどめ、詳しくは直接リポジトリを参照してもらうという形にしたいと思います。
なお、筆者は非ソフトウェアエンジニアということもあり、今回紹介する設計や実装の中には技術的に拙い部分があるかと思います。もし本稿の中で間違っている記述などありましたらご指摘いただけると助かります。
本稿がこれからRustでBot開発を始めようとしている方や、既に開発を進めている方にとって少しでも参考になれば幸いです。
設計
アーキテクチャ
今回は、システム全体を大きく4つのレイヤに分けてみました。
後述するように、それぞれのレイヤに対応するインタフェースをトレイトとしていくつか定義しています。実際のBot運用に向けては、関心のある「取引所」や「取引戦略」に対してこれらのトレイトを実装していくことになります。
exchange
取引所サーバとのコミュニケーションを担うレイヤです。各取引所が(取引所APIを通じて)提供すべき共通のふるまいをMarket
、Status
、Broker
という3つのトレイトに定義しています。これらのトレイトを関心のある取引所の仕様に応じて実装することで、その取引所に対してBot取引が可能になります。
strategy
取引戦略にしたがって次のアクション(注文リクエスト)を決定するレイヤです。後述するように、実行したい取引戦略のロジックをPolicy
トレイトの実装としてコード化することで、そのロジックをBotで実行することができるようになります。
database(未実装)
exchange
から受信したリアルタイムデータを蓄積し、時系列データ等の形式で提供します。板情報や約定履歴などの生データだけでなく、ローソクデータやインジケータなどの二次加工データを利用する際もここの蓄積データから作成することになると思います。今回は時間の都合上、未実装です。
bot
上述の3つを総合的に操作して、Bot全体としての機能を実現します。
大まかな処理の流れは以下の通り。
-
Market
とStatus
からデータをリアルタイム受信 - 受信データに応じて最新の観測状態を表す
Observation
値を逐次アップデート - 2をトリガーに
Observation
値をPolicy
に入力し、実行アクションを決定 - 実行アクションが存在する場合、
Broker
とOrderService
を介して実行
メッセージング
Market
とStatus
のリアルタイムデータは、PubSub方式で配信するようにしました。詳細については割愛しますが、Publisher側でPubSub<T>
を作成してメッセージを送信、Subscriber側はSubscription<T>
を通じてT
型のメッセージを受信します。
以下はString
型のメッセージを配信する例です。
Publisher側
// Create pubsub value
let pubsub: PubSub<String> = PubSub::new();
// Publish
pubsub.publish(String::from("hello,"));
pubsub.publish(String::from("botter"));
Subscriber側
// Subscribe
let subscription: Subscription<String> = pubsub.subscribe();
// Received!
assert_eq!(subscription.recv().unwrap(), String::from("hello,"));
assert_eq!(subscription.recv().unwrap(), String::from("botter"));
今回のシステムでは、exchange
がPublisher側、bot
がSubscriber側となります。
インタフェース
Market
pub trait Market {
fn info(&self) -> MarketInfo;
fn orderbook(&self) -> Subscription<Orderbook>;
fn execution(&self) -> Subscription<Execution>;
}
Market
トレイトを実装する型は、Orderbook
(板)やExecution
(約定)といった価格や出来高に関するリアルタイムデータを配信する役割を持ちます。
実際の取引所のリアルタイムAPI(WebSocketなどで受信)では、板情報の更新に関する差分データ(create/update/delete)を受け取りローカルでスナップショットを更新していく形式が一般的ですが、今回の設計ではMarket
から配信されるOrderbook
型はある時刻における板情報のスナップショットであり、前述の差分更新などの処理はMarket
実装側の責務としています。
Status
pub trait Status {
fn inventory(&self) -> Subscription<Inventory>;
fn open_orders(&self) -> Subscription<OpenOrders>;
}
Status
トレイトを実装する型は、Inventory
(ポジションや現物残高などの在庫情報)とOpenOrders
(アクティブな指値注文のリスト)のリアルタイムデータを配信する役割を持ちます。
「取引所データの配信」という意味ではMarket
トレイトと役割が似ていますが、こちらは自己アカウントに関するプライベートデータ(通常apikeyによるauthenticationを要するようなもの)を担当します。
OpenOrders
については、前述のOrderbook
と同じくスナップショットです。
Broker
#[async_trait]
pub trait Broker {
async fn submit(&self, order: Order) -> OrderResponse;
}
Broker
トレイトを実装する型は、注文リクエストを取引所サーバに送信する役割を持ちます。
Broker::submit
メソッドは、引数として受け取ったOrder
値に応じて注文リクエストを作成して取引所に送信し、その結果(注文リクエストが成功したか失敗したか)を返します。
Market
やStatus
との通信は一方通行(データ受信のみ)であるのに対し、Broker::submit
はリクエスト後に取引所からのレスポンスを待ち受ける必要があり、それにより(少なくない)待機時間が発生します。これをtokio
などの非同期ランタイム下で処理するためにasync fn
として定義しています。
実際にBroker
に注文リクエストを投げる際は、OrderService
を介して行います。OrderService
はBroker
経由で注文リクエストを送信するとともに、それらのリクエストを追跡し、まだレスポンスを受け取っていないリクエストをVec<PendingOrder>
として管理します。bot
からはget_pending_orders
メソッドでVec<PendingOrder>
を取得し、Policy
によるアクション決定に使用します。
Policy
pub trait Policy {
fn evaluate(&self, observation: impl Observation) -> Vec<Order>;
}
Policy
トレイトを実装する型は、現在の観測情報から次の実行アクションを決定する役割を持ちます。
Policy::evaluate
メソッドは、Observation
トレイトを実装した型から、現在の観測情報に関するデータを読み取り、それらに基づいて次にどのようなアクションを起こすか(新規注文する?キャンセルする?何もしない?)を計算します。決定した一連のアクションはVec<Order>
として出力されます。
Observation
のトレイト定義は以下のとおり全てがgetterメソッドであり、これらの値が観測情報として提供されます。
pub trait Observation {
fn info(&self) -> &MarketInfo;
fn executions(&self) -> &[Execution];
fn orderbook(&self) -> &Orderbook;
fn inventory(&self) -> &Inventory;
fn open_orders(&self) -> &OpenOrders;
fn pending_orders(&self) -> &[Order];
}
基本型
高頻度Botトレーディングのドメインにおける基本的な概念を定義しています。ここで定義された型は、exchange
とやりとりする各種データの共通フォーマットとなる他、Policy
実装にてアクション決定のための計算に使用されます。
Decimal
, Price
, Amount
use rust_decimal::prelude::*;
pub type Price = Decimal;
pub type Amount = Decimal;
Price
は「価格」の値を表し、Amount
は「数量」の値を表す型です。
便宜上わかりやすい名前を与えていますが、これらはともにrust_decimal
クレートのDecimal
型のエイリアスです。
今回のシステム内での価格と数量に関する数値計算は基本的にDecimal
型で行うことになります。
Execution
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TradeId(String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Execution {
timestamp: u64,
id: TradeId,
maker_side: Side,
price: Price,
amount: Amount,
}
Execution
は、ある時点で発生した約定イベントを表します。
TradeId
は各Execution
値を識別するIDです。これは取引所側で発行されるIDで、Bot側でもそのままの値を使用しています(つまり、一意性については取引所をfull trustしています)。
ちなみに、このIDの実体は単なるString
型ですが、そのまま使用すると後述する他のID(OrderId
やOfferId
など)と混同する危険があるため、TradeId
型にラップして静的に区別しています[3]。
Orderbook
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct OfferId(String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Offer {
pub(crate) id: OfferId,
pub(crate) price: Price,
pub(crate) amount: Amount,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Orderbook {
pub(crate) timestamp: u64,
pub(crate) asks: Vec<Offer>,
pub(crate) bids: Vec<Offer>,
}
Offer
はOrderbook
の上に存在する1つの指値注文(もしくはそれらを価格ごとに束ねたもの)を表します。
OfferId
は各Offer
値を(あるOrderbook
値の内部で)識別するためのIDです。
TradeId
と同じく、取引所側で発行されたIDを直接使用します。
取引所によってはOfferId
は後述するOrderId
と概念的に同一である場合もありますが、そうでない取引所も存在するので、今回は別々の型で表現することにしました。
Orderbook
はその名のとおり、ある時点での板情報のスナップショットを表す型です。内部ではasksとbidsがそれぞれVec<Offer>
として保持されていて、それぞれbest側のOffer
から順に配置されています。
Orderbook
に対しては、Market
実装での差分更新処理の際にmutableな操作が必要になりますが、それらの差分更新ロジックはexchange
側の知識であるため、Orderbook
本体にsetterを生やすのではなくOrderbookWriter
というラッパー型に切り分けて定義しています(各メンバのvisがpub(crate)
となっているのはこのため)。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OrderbookWriteOp {
Snapshot(Orderbook),
Create(CreateOp),
Update(UpdateOp),
Delete(DeleteOp),
}
pub struct OrderbookWriter<'a> {
inner: &'a mut Orderbook,
}
impl<'a> OrderbookWriter<'a> {
pub fn apply_snapshot(&mut self, orderbook: Orderbook) -> OrderbookWriterResult<()> {
...
}
pub fn apply_create(&mut self, op: CreateOp) -> OrderbookWriterResult<()> {
...
}
pub fn apply_update(&mut self, op: UpdateOp) -> OrderbookWriterResult<()> {
...
}
pub fn apply_delete(&mut self, op: DeleteOp) -> OrderbookWriterResult<()> {
...
}
...
}
Inventory
use super::values::Amount;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Inventory {
Position(Amount),
Balances(Balances),
}
impl Inventory {
pub fn position(&self) -> Amount {
match self {
Self::Position(position) => *position,
Self::Balances(balances) => balances.base_amount(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Balances {
ba: Amount,
qa: Amount,
}
impl Balances {
pub fn new(ba: Amount, qa: Amount) -> Self {
Self { ba, qa }
}
pub fn base_amount(&self) -> Amount {
self.ba
}
pub fn quote_amount(&self) -> Amount {
self.qa
}
}
Inventory
は、あるマーケット(取引ペア)における自己アカウントの在庫状況を表す型です。この型はPosition
とBalances
からなるenum
であり、信用取引の場合はPosition
、現物取引の場合はBalances
(base/quoteの現物残高)となります。現物ペアの場合はInventory::position
メソッドでbase残高をポジションとみなしてその値を返します。
Order
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct OrderId(String);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Side {
Ask,
Bid,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum OrderType {
Limit,
Market,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Order {
New(NewOrder),
Cancel(CancelOrder),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewOrder {
order_type: OrderType,
order_side: Side,
price: Price,
amount: Amount,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CancelOrder {
id: OrderId,
}
数が多くて大変です。Side
とOrderType
は自明なので省きましょう。
OrderId
はリクエスト済みの注文に対して取引所側から付与されるIDで、注文のキャンセルなどで注文を指定する必要がある場面で使います。
Order
は、注文リクエストを表す型で、New
(新規注文)かCancel
(キャンセル)のどちらかをとります。Broker
実装ではこの値を解釈して取引所向けのリクエストを作成します。
OpenOrders
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpenOrders {
pub(crate) timestamp: u64,
pub(crate) orders: Vec<OrderState>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrderState {
pub(crate) id: OrderId,
pub(crate) side: Side,
pub(crate) price: Price,
pub(crate) amount: Amount,
}
OpenOrders
は、自己アカウントのアクティブな指値注文のリストを保持します。指値注文はOrderState
として格納されています。OrderState
のamount
は「約定済み」ではなく「未約定」の数量であることに注意してください。
なお、OpenOrders
もOrderbook
と同様に、exchange
側での差分更新処理が必要であるため、mutable操作を許可するOpenOrdersWriter
というラッパーを別途用意してあります。
実装
一応、ここまででフレームワーク部分は形になりました。疲れました。画面の右上にチラチラ見える「公開する」ボタンを押してしまいたいです。
が、ここで終わってしまうと
このシステム、本当に使えるんけ????????
という疑問が(筆者本人も含めて)残り続けてしまうので、strategy
とexchange
についてそれぞれ一つずつ試しに実装してみようと思います。
sample strategy
「過度に利益志向な内容にならないように」というBotterアドカレの厳粛なる教義にしたがい、儲かる取引戦略を載せるのは遠慮しておきましょう。残念ですが仕方ない、、
とはいえ、「儲からない取引戦略を適当に考えろ」というのも結構難しいのですが、、幸運なことにその昔、以下のような記事を書いたことを思い出しました!
おお、これはまさに我々がほしかった(今はたぶん)儲からない取引戦略[4]です!!!!
あらためて見るとかなりプレーンなMarketMakingですが、サンプルとしては好都合です。このロジックを今回作ったシステムに移植してみましょう。
#[derive(Debug)]
pub struct DepthBasedOffering {
max_exposure: Amount,
target_depth: Amount,
}
impl DepthBasedOffering {
pub fn new(max_exposure: Amount, target_depth: Amount) -> Self {
Self {
max_exposure,
target_depth,
}
}
pub fn max_exposure(&self) -> Amount {
self.max_exposure
}
pub fn target_depth(&self) -> Amount {
self.target_depth
}
}
取引戦略の実行に必要なパラメータを格納しただけのシンプルな構造体DepthBasedOffering
を定義[5]しました。
パラメータは以下の2つ。
max_exposure
:保有ポジション(の絶対値)の上限
target_depth
:Best Ask/Bidから積算してどれだけのサイズ(depth)の位置に指値を置くか
(詳しい取引ロジックは上記noteを参照ください)
さて、あとやることは一つだけ、Policy
トレイトの実装です!
impl Policy for DepthBasedOffering {
fn evaluate(&self, observation: impl Observation) -> Vec<Order> {
if !observation.pending_orders().is_empty() {
return Vec::new();
}
let mut orders = Vec::new();
// some logic
orders
}
}
長ったらしいのでロジックの中身は省略しました。
最初のガード節でpending_orders
が存在しないことを確認しています。これは、直前に出力済み(かつ送信済み)のVec<Order>
と同一結果を再出力しないようにするためです。
(この部分、やり方によってはobservation
とpending_orders
から信念状態のようなものを導出してそれに基づいてアクション決定するみたいなこともできそうですが、今回のはシンプルにフェイルセーフな感じにしています。)
で、そのあと、ロジックに沿ってorders
の中にOrder
値を適宜push
していって、、最後にそれらをリターンしています。
ソースファイルの後方ではこの取引戦略の実装の正しさを確認するテストをいくつか書いています。(Bot本体から分離されているのでユニットテストも簡単に書けますね!)
sample exchange
今日はクリスマスです。
つまり、あと一週間で今年が終わるということです。
今年が終わるとどうなる?
しらんのか
確定申告が始まる
、、、
何が言いたいかというと、儲かりもしない高頻度Botなんか動かして取引履歴をむやみやたらに汚したくありません!!!!
でも今回作ったシステムは試してみたい、、なにかいい方法はないでしょうか?
あります。取引所のテストネットAPI(いわゆるデモ口座的なやつ)を使うことにしましょう。
でも国内にはありません[6]。仕方ないので海外取引所にしましょう。
というわけで、今回は(まさかの)BitMEXという取引所のテストネットAPIを利用します。
テストネットAPI
前半に説明したとおり、ある取引所に今回のシステムを適合させるためには、その取引所についてMarket
、Status
、Broker
をそれぞれ実装しなければなりません。
当然ではありますが、そのための作業の中には「取引所サーバに接続して」「必要なデータを収集して」「正しい形式にパースする」というような、具象と技術的詳細にまみれた、煩わしくて、面倒臭くて、吐き気を催すコーディングも含まれます。
でもよう、そんなことするためにBotterになったわけじゃあねえだろう
まあ落ち着いて聞いてください。これも非常に幸運なことですが、今日はクリスマスです。しかも2022年の。
そう、この時代にはもう既にアレが存在しているのでした。頼りましょう!
わあしゅごい。
ちなみに、このあと調教質問を続けていったところ、どうやらbitmex-rs
というクレートを使うともっと高水準な感じでBitMEX APIを叩けそうなことを教えてくれました。Cargo.toml
に追加しましょう。
bitmex = "0.2.2"
関連するクレートもまとめて追加しておきます。
tokio = { version = "1", features = ["full"] }
futures = "0.3"
ここで重大なことに気づいたのですが、このbitmex-rs
というクレート結構古いっぽく(エラーハンドリングにfailure
クレートが使われてたりとか)、なんとtokio
の依存バージョンが0.2
であるために最新の1.*
のランタイムでは動きませんでした、、
と思いきや、crates.ioではなく、gitリポジトリを直接指定したら動きました!
bitmex = { version = "0.2.2", git = "https://github.com/dovahcrow/bitmex-rs.git" }
...
...
というわけで、紆余曲折を経ましたが、どうにか三種の神器であるBitMEXMarket
、BitMEXStatus
、BitMEXBroker
を用意することができました!!
このへんに突っ込んどいたよ↓
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── implements
│ │ ├── exchanges
│ │ │ ├── bitmex
│ │ │ │ ├── broker.rs
│ │ │ │ ├── market.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── parser.rs
│ │ │ │ └── status.rs
はあ〜もうしにそうです。でもあとは動かすだけや、、
main
関数
ああ、そうだった。動かすためにはmain
を書かなくちゃ。
extern crate market_maker;
use rust_decimal_macros::dec;
use market_maker::apikey::ApiKey;
use market_maker::bot::{Bot, Config};
use market_maker::implements::exchanges::bitmex::{BitMEXBroker, BitMEXMarket, BitMEXStatus};
use market_maker::logger;
use market_maker::strategies::dbo::DepthBasedOffering;
fn main() {
logger::setup_with("info");
// exchange
let apikey = ApiKey::read_json("./keys/bitmex.json").expect("apikey not found");
let market = BitMEXMarket::connect();
let status = BitMEXStatus::connect(&apikey);
let broker = BitMEXBroker::connect(&apikey);
// strategy
let policy = DepthBasedOffering::new(dec!(200), dec!(1000));
// bot
let config = Config {
num_iteration: usize::MAX,
test: true,
};
let mut bot = Bot::new(config, market, status, broker, policy);
bot.run().unwrap();
}
とってもシンプルですね。これまで定義してきたトレイトを実装する各型をインスタンス化して、それらをまとめてBot
のコンストラクタにぶちこんでいます。取引所や取引戦略を別のにすげ替える場合も、具体型が変わるだけなのでmain
の中身はほぼ同じ見た目になると思います。
Config
はBot
自体の設定データで、test: true
とすると(Policy
からVec<Order>
は出力されますが)注文リクエストは送信されなくなります。
なお、BitMEXテストネットに接続するためのAPIキーはJSONファイルに格納しておいて実行時に読み取るようにしています(適切なオーナーシップ設定を忘れずに!)。
{
"key": "YOUR_KEY",
"secret": "YOUR_SECRET"
}
メインループはBot::run
の中にあります。
impl<M, S, B, P> Bot<M, S, B, P>
where
M: Market,
S: Status,
B: Broker + Send + Sync + 'static,
P: Policy,
{
pub fn run(&mut self) -> Result<()> {
info!("Start running!");
info!("\n{:#?}", self.config);
let info = self.market.info();
info!("\n{:#?}", info);
let execution = self.market.execution();
let orderbook = self.market.orderbook();
let inventory = self.status.inventory();
let open_orders = self.status.open_orders();
info!("Warmingup observation..");
let mut observation = Observation::warmup(
info,
execution.as_receiver(),
orderbook.as_receiver(),
inventory.as_receiver(),
open_orders.as_receiver(),
)?;
for i in 0..self.config.num_iteration {
let mut target = false;
select! {
recv(execution.as_receiver()) -> msg => {
info!("iteration[{i}] receive execution!");
observation.insert_execution(msg?);
},
recv(orderbook.as_receiver()) -> msg => {
info!("iteration[{i}] receive orderbook!");
observation.update_orderbook(msg?);
target = true;
},
recv(inventory.as_receiver()) -> msg => {
info!("iteration[{i}] receive inventory!");
observation.update_inventory(msg?);
},
recv(open_orders.as_receiver()) -> msg => {
info!("iteration[{i}] receive orders!");
observation.update_open_orders(msg?);
},
}
if target {
let pending_orders = self
.order_service
.get_pending_orders()
.into_iter()
.map(|po| po.into_inner())
.collect();
observation.update_pending_orders(pending_orders);
info!("orderbook:\n{}", observation.orderbook());
info!("open_orders:\n{}", observation.open_orders());
info!("inventory:\n{:?}", observation.inventory());
info!("pending_orders:\n{:?}", observation.pending_orders());
info!("iteration[{i}] evaluating..");
let orders = self.policy.evaluate(&observation);
info!("output:\n{:#?}", orders);
if !orders.is_empty() && !self.config.test {
for order in orders {
self.order_service.submit(order);
}
}
}
}
Ok(())
}
}
各Subscription
を開始した後、Observation
初期値を構築し、ループに入ります。
各イテレーションでは最初にselect!
マクロ[7]で全てのreceiver
をまとめて待ち受け、最初に受信したメッセージに対してObservation
値の更新処理を実行します。
現在の実装ではupdate_orderbook
のタイミングでPolicy
計算を開始するようにしています。
Run!!!!
パラメータは以下の設定です。
DepthBasedOffering {
max_exposure: dec!(200),
target_depth: dec!(1000),
}
test: false
で注文機能をオン。
let config = Config {
num_iteration: usize::MAX,
test: false,
};
さあ、いよいよです。--release
フラグをつけてBotを起動しましょう!
cargo run --release --example run_dbo_bitmex
わくわく。
、、、
あっ!早速、最初の指値注文が出力されました。
[
New(
NewOrder {
order_type: Limit,
order_side: Ask,
price: 16949.0,
amount: 200,
},
),
New(
NewOrder {
order_type: Limit,
order_side: Bid,
price: 16816.5,
amount: 200,
},
),
]
UIで確認してみましょう。しっかりと注文が反映されています!!
パラメータに設定したとおり、板のトータルサイズが1000
となる価格位置(16949.5
/16816.0
)の手前にそれぞれ200
ずつの指値が置かれています。
指値位置の変更(Cancel
→New
)も問題なく実行されています。
おやおや、買い指値の一部が約定し、保有ポジションが+100
になりましたね。
今回の設定ではポジション絶対値の上限は200
なので、買い指値は100
、売り指値は300
に調整されています。これも期待通り!!!!
、、、
、、、
ふう、、、、
楽しかったですね。
願わくばこのまま一日中眺めていたいところですが、さすがにもう体力の限界です。
基本的な動作は一通り確認できたことですし、今回のテスト運用はこのへんにしておきましょう!!
課題
めでたく動きました。
しかし、本稿を執筆するためだけに生み出された存在である本システムは言うまでもなく荒削りのプロトタイプであり、おそらく改善点やバグも数えきれないほど残されていることでしょう。
いくつかは本文で触れましたが、それ以外ですぐに思いつくものを以下に挙げておきます。
-
Observation
のデータ整合性
現在の設計では、Market
とStatus
からの各データは、データの発生順ではなく到着順で受信され、Observation
に順次適用されています。そのため、タイミングによってはObservation
の値全体になんらかの不整合(例えば、OpenOrders
は最新であるのにOrderbook
にはそれらに対応するOffer
が反映されていない、など)が発生する可能性があります。 -
Observation
に保持されるExecution
サイズが無制限
Botの連続稼働時間やデータの蓄積速度によってはメモリを圧迫する可能性があるので、キャパシティを設定する必要があります。 -
レートリミット
取引所のレートリミット(単位時間あたりの最大リクエスト数)を考慮して注文リクエストを制限する機構をどこかに設置する必要があります。
さいごに
当初の想定をはるかに超える長旅となってしまいましたが、なんとかタイトルのとおり「RustでMarket Maker Botを作ったよ!」と胸を張って言えるくらいのところまでは辿り着けたのではないでしょうか。
まあ、今回提示した「フレームワーク」というスタイルはあくまでもBot取引を実現する手段の一つに過ぎないので、各自の状況に合ったスタイルを選ぶのが良きかなと思います。フレームワークに括り出せる部分が少なかったり、処理の共通化にともなうオーバーヘッドが気になるというのであれば、それぞれ独立したプログラムに分ける、といった風に。
あとあと、今回は「Bot取引」という大きなトピックの中でも特に技術的な側面にフォーカスした内容になりましたが、実際にBot取引で利益を生み出すためには、プログラムの実行速度や開発効率の他にも重要な要素が山ほどあります。さらにドメイン同士を比較してみても(CeFi vs DeFiとか、arb vs directionalとか)それらの重みが異なっていたりもするので、(技術面にだけ盲目的に傾倒するのではなく)状況に応じてバランスをうまくとりながら取り組んでいけたらいいですね!
、、、さてついに意識が途切れ途切れになりつつあるので、今回はこのあたりで終わりにしたいと思います。
最後まで読んでいただきありがとうございました。
それでは良いお年を!
追記
投稿直前に知ったのですが、Botterアドカレにて素晴らしい記事とクレートが紹介されておりました!
本稿でも一番苦労したのがAPIクライアントの実装まわりだったので、このあたりを受け持ってくれるRustクレートの登場は嬉しいですね。次回からありがたく活用させていただきます!
-
Bot: 本稿では「暗号資産の取引を自動実行するプログラム」を指します。 ↩︎
-
裁定取引(arbitrage)や清算(liquidation)などが有名です。 ↩︎
-
NewTypeパターンといいます。参考:https://doc.rust-jp.rs/book-ja/ch19-04-advanced-types.html ↩︎
-
実は記事公開後もめちゃくちゃ儲かったという噂があります。 ↩︎
-
取引戦略の名前は筆者が勝手につけました。正式名称かもっとかっこいい名前があれば教えてください。 ↩︎
-
APIの仕様を調べるためだけに実弾使うの結構しんどかった思い出があります。これあるとBotter's UX全然違うと思います。人気出るのでは。導入いかがでしょう?bitbankさんとか。 ↩︎
-
select!
マクロはtokio
クレートのものが非同期コードでよく使われていますが、実はcrossbeam
クレートにも生スレッド用に同様の機能を提供するマクロがあり、今回はそれを使っています。 ↩︎
Discussion