仮想通貨Botterに贈るRust×bot開発 あるある21連発
はじめに
仮想通貨botterのみなさん、こんにちは!
この記事は仮想通貨botter Advent Calendar 2024 の二日目の記事です!
前日の記事は5000e12さんによる、ゆるく始めるアトミックアービトラージ と、shun_m2eさんによる SUIチェーンでAtomic Arbを行うまで でした。
どちらの記事もAtomic Arbを異なった側面から説明していてとても参考になりました✨
この記事では、bitbankでの高頻度取引botをRustで開発中の私が、なんかカッコいいプログラミング言語であるRustの魅力とその使い勝手を象徴するあるあるなどを紹介します。
Rust経験のないBotterさんも、この記事を通してRustへの親しみが増して、Rustでbotを書きたくなってくるかもしれません!
Let's become Rustacean! 🦀
Pythonユーザーに思われていそうなこと編
1. 「Rustじゃ機械学習できないじゃん」と思われていがち
おそらくPythonでbotを開発している人の半分くらいは、Pythonまわりの機械学習ライブラリが充実していることを理由にPythonを選んでいるのではないでしょうか?
そんなあなたに朗報です! RustでもPythonで作ったモデルを実行することができます!
詳細は省略しますが、ONNX[1] という機械学習モデルを表現する一般的なフォーマットにモデルを変換して、それをONNXランタイムのRustラッパー(ort: https://github.com/pykeio/ort) を使って実行することで、たいていの機械学習モデルはRustで実行できるはずです。
私はPythonで決定木系のモデルを学習させて、そのモデルをRustで実行させる方針で開発しています!
2. 「型を毎回書くのがダルいよね」 と思われていがち
Rustは静的型付け言語なので、変数の定義のたびに型を毎回書かなくてはいけないと思っている方もいるかもしれません。
しかし、Rustでは型推論が働くことに加えて、VSCodeなどのモダンな環境では、rust-analyzerというツールを使うことで型推論の推論結果を見られたりと、型を書かずに型を使うようなプログラミングが可能です

rust-analyzerが働いている様子(limit_info_buysの型がVec<LimitInfo>であると推論されたことが表示されている)
Rustのここがありがたいよね編
3. Debugをマクロで導出するとなんでも雑にprintできるのマジありがとうがち
Pythonで複雑なクラスの中身を見ようとしてprintしたものの <__main__.HogeClass object at 0x...>という出力が出て困惑したことがあるみなさんも多いのではないでしょうか?

本当に見たい情報はself.aに10が入ってることなどだが、オブジェクトのアドレスを教えてくれる
Rustでは、複雑な構造体やEnumも(手続き型マクロという謎の力により)#[derive(Debug)]と書き加えるだけで簡単に出力できるようになったりします。

#[derive(Debug)]の1文を加えるだけで、こんな複雑な構造体もprintln!{:?}などでデバッグ出力できるようになる
4. Option<T>とResult<T,E>はかなり偉いがち
Rustには、Enum(列挙型)という型があり、取りうる値を全部列挙することで定義されます。
OptionとResultもこのEnumの一種です。 Option<T>はSome(T)とNoneのみからなり、[2]データがある場合とない場合を表すときなどに使われがちです。
Result<T,E>はOk(T)とErr(E)からなり、取引所からのレスポンスなど、成功するかわからないものをResultで表したりしがちです。
5. どこでエラーを握りつぶしているのかわかるの助かりがち
先ほど紹介したOption<T>やResult<T,E>でTに相当するものを得るためには、Tがない場合(OptionならNoneだった場合。ResultならErrだった場合)にpanicしてしまうことを覚悟してunwrap()を呼ぶか、パターンマッチングなどで場合分けする必要があります。

画像のように、Option<i32>とi32の整数は定義されていないので怒られる。 34行目のpiyoの定義の部分では、
プログラマーがOptionの中身はSomeだと確信していることがunwrapして足していることからもうかがえる。
この制約は一見、データがあるのが当たり前の状況でもunwrapを書かせるだけの冗長な制約に思えるかもしれませんが、プログラマーの責任や覚悟をunwrapとして表せるというメリットがあります。

横着してErrが返ってくる状況でもunwrap()を呼んでしまっているコードを、ちゃんと処理するようにしたcommit
自分がどこで怠惰になったのか、エラーと向き合わずに横着したのかも同時にわかって、人間としての欠点も認識してしまいがち。
6. ライブラリのドキュメントがしっかりして(る傾向がある)いるのに感謝する日々がち
助かるねぇ
7. コンパイラやエディタがエラーの場所を事前に教えてくれるのありがたいがち
Pythonなどの動的型付け言語を使っていると、全くノーマークだった部分でのType Errorが起きて絶望するのは日常茶飯事でしたが、Rustは静的型付け言語なのでこういったエラーは実行前に検知できます!
Rustのここはあまり嬉しくないよね編
8. コンパイルに数分かかって困りがち
長いて。自分の環境だと1分くらいで終わるけど、長い場合5,6分かかることもあるそうです。
9. Dockerコンテナイメージにするとクソ重いがち
工夫しないとイメージが2GiBほどになったりします。困る。(ちゃんとマルチステージビルドすると120MBくらいになります。)

10. GCPの公式SDKないのちょっと困りがち
有志が開発しているクレートはあるので私はそれを使っていますが、やっぱり公式SDKがあればそっちを使いたいです。
Google聞いてるか?

Google Cloud SDK for Rustはない
11. ライブラリないがち
(どういう文法??)
Pythonなどの言語だと、仮想通貨取引所のAPIはたくさん充実していますが、それに比べるとRustではそれほどには充実していません。
しかしなんと、bitbankでのAPI取引をサポートするRustライブラリがあるらしいですよ!?
私が書いたこのライブラリがもしあなたの役に立ったなら、ぜひGitHubリポジトリでスターをつけて応援してください!
あなたの「いいね!」が開発のモチベーションに繋がります!
Rust始めたてのあるある
12. The Rust Programming Language、実は長いがち
The Rust Programming Language(TRPL)といえば、Rustのチュートリアル的存在(the bookと呼ばれるほどに!)ですが、実はTRPLはかなり長いです。
1章がわりと重く、全体で20章あるので私は何度も挫折しました。
私は夏休みにまとめてTRPLを全部履修したのですが、15日間ほどかかりました…
1日に数時間やっていたので、合計で30から45時間ぐらいかけたのではないでしょうか?
純あるある
13. ライフタイム意味わからないがち
Rustには参照がどれくらい有効であるかを考えるライフタイムという概念があり、特定の状況においては、変数のライフタイムを自分で指定しなければいけません。ですが、これがとても難解でめちゃめちゃコンパイラに怒られることになりがちです。
14. ライフタイムが意味わからないので、とりあえず&strは避けておきがち
ライフタイムや参照などの色々が関わり合う結果として、&strを扱うよりもStringを扱いたくなりがちです。
15. Rustの速さが活きる場面あんまりないがち
RustといえばC++級に速い言語だという印象があると思いますが、実はRustの速さを実感できるような場面はCEX取引においてはあまりありません。
取引所の通信をすると否応なしに数十ms~数百msかかるので、Rustの処理速度の影響はあまり大きくないんですね。(CPUバウンドの計算をたくさんするような戦略であれば話は変わってくるかもしれません)
16. コード長くなりがち
これは必ずしもRustの特徴とは言えない気がしてきました。型注釈をつける言語あるあるでしょうか。
実際Rustで書いてるbotプログラムは経験的には長くなりがちです。

取引所のAPIを叩いて注文を出す処理だが、型注釈のおかげで1文がめちゃめちゃ長くなっている
17. CopilotやAIアシスタントのコード補完の成功率、明らかに低いがち
他の言語での経験と比べると、Rustでは非同期プログラミングなどが絡んだり、ちょっと難しい処理が入ったりすると、明らかに生成AIのコードの質は悪くなってしまう感覚があります。 Rustの学習データが足りないからなのでしょうか? それとも、非同期プログラミングなどが本質的に難しいからでしょうか?
18. 非同期プログラミング難しいがち
そもそも非同期プログラミング自体が難しいです。
普通の同期的なプログラミングと違い、処理される順番がコードの書いた順番ではなかったり、Futureやそれを処理するtokioランタイムの理解もする必要があるからです。
19. メモリ共有の本質的難しさを認識した後に使うchannel、革命がち
非同期プログラミングで状態を共有して扱うのは難しく、Tという型を共有して扱いたい場合はArc<Mutex<T>>としてごちゃごちゃする必要があるのですが、channelというのを使うときれいに書けてびっくりしがちです。
たとえば、WebSocketの通信を受け取ったたびにchannelを通してデータを渡す処理はこのように書けます!キレイ!(当社比)

データを受け取ったらchannelを通じて送る

チャネルから送られてきたデータについて処理する
20. こういった非同期の難しさを簡単に扱えると噂されているGoに嫉妬しがち
GoではRustの非同期プログラミングの難しいいろいろな要素(ライフタイムなど)がないらしく、言語側でうまくやってくれるという噂が常に飛び交っています。 羨ましいですね。
21. 「Rustの堅牢なメモリ管理や高パフォーマンスが好き」という気持ちよりも、「Rustという難しい言語を扱っている自分カッケーェw」という気持ちのほうがやや大きいがち
堅牢な言語よりも、堅牢で難しい言語を飼いならしてプログラミングしてる自分のほうがカッコいいものです。
さいごに
この記事では、Rustでbitbankの高頻度取引botを開発している私が感じるRustのあるあるをPythonistaやRustaceanに向けて紹介しました。
共感できる部分はありましたか?
Rustは難しい言語ではありますが、実はその難しさは扱っている対象の難しさを映し出しているだけだったりします。
この記事を読んで、少しでもRustでのbot開発に興味を持っていただけたら幸いです。
もしよろしければ、この記事に「いいね」を押していただけると嬉しいです!
質問や感想/指摘など、どんなコメントもお待ちしています!
Discussion