LoginSignup
17
6

More than 1 year has passed since last update.

Rust版pybotters、crypto-botters

Last updated at Posted at 2022-12-23

0. この記事について

この記事はHohetoさんの 仮想通貨botter Advent Calendar 2022 の24日目(裏)の記事です。ぜひ最後まで読んでいってください!

こんにちは。ねぎ草(@negi_grass)と申します。
crypto-botters(GitHub)というRustのライブラリを開発しているのでその紹介をしようと思います。貢献してくれる方は、是非issueを立てたりプルリクを送ったりしてください!

大変申し訳ございません。何も考えずにハイフンをクレート名に使ってしまいました。使うときは use crypto-botters; ではなく use crypto_botters; と書いてください。

最新バージョンは、この記事を書いた当時のコードと大きく異なっていますGitHubのREADMEが最新の内容です。(generic-api-clientの部分は変わっていません。)

0.1. 目的

ライブラリをOSSとして公開しているのは、Rustのbottersのコミュニティに貢献するためです。Rustを使っているbotterは一定数いるはずなのに、あまりRustに関する情報交換が活発でないような気がしたので、その活性化のきっかけになれればと思います。

0.2. 何のライブラリか

タイトルにもある通り、完全に同じものを目指しているわけではありませんが、まちゅけんさんのpybottersのRust版的なものです。(私が勝手に思ってるだけです。)できることは似ていますが、DataStoreの機能が(まだ?)なく、対応しているのはまだBinaneとbitFlyerだけです。また、WebSocketの自動再接続に少しだけ力を入れています。

また、非同期、特にtokioのランタイムの中で使われることを想定しています。そうでない場合はエラーが出ると思います。

1. Rustのメリット

まずはじめにRustの宣伝をさせてください。(Rustを使っている方は飛ばしてください。)ここでは、Rustメリットを3つ紹介します。

1.1. 静的型付けコンパイル言語である

コンパイル時に型チェックが実行されるため、実行時に型を間違えているためにプログラムが死ぬ、なんてことは起きません。またライブラリを使うとき、関数の引数と返り値の型はすべてソースコードに書いてあるため、いちいち使い方をググる必要がありません。(私が動的型付けがあまり好きではない第一の理由です。)コード補完もしっかり利きます。

1.2. 高速である

静的型付けコンパイル言語である上、ガベージコレクションが無いため非常に高速です。(これは所有権という仕組みのお陰です。もちろん手動でmallocfreeなどを呼ぶ必要はありません。)

bot を作る上では

  • バックテストをするとき
  • アビトラやマーケットメイクなどの戦略を実行するとき

などで役立つと思います。

1.3. 公式ツールが優秀

RustにはCargoという公式ツールがあり、Cargo.tomlというファイルに依存関係を書いておくことで、コマンド一つで

  1. 依存関係を調べ、同じパッケージは互換性のあるバージョンならまとめる
  2. ダウンロードしていないパッケージのダウンロード
  3. 変更されたパッケージをコンパイル
  4. 実行

をやってくれます。他にも機能はいろいろあります。

2. パッケージの構造

crypto-bottersは1つのワークスペースの中の、現在は4つのクレートでできています。ディレクトリ構造は以下の通りです。

crypto-botters
├─crypto-botters-binance
│   ├─src
│   └─Cargo.toml
├─crypto-botters-bitflyer
│   ├─src
│   └─Cargo.toml
├─generic-api-client
│   ├─src
│   └─Cargo.toml
├─src
└─Cargo.toml

crypto-bottersがワークスペースのルートパッケージです。これらの依存関係は以下の通りです。
dependency.jpg
crypto-botters-binancecrypto-botters-bitflyergeneric-api-clientに依存していて、それらすべてにcrypto-bottersが依存しています。

それぞれのクレートの役割について説明します。

2.1. generic-api-client

generic-api-clientには実際にHTTPリクエストを送ったり、WebSocketを接続したりするコードが含まれます。 generic という名前の通り、どんなAPIにも対応できるような設計のため、認証やデータのパース等は行いません。RequestHandlerWebSocketHandlerというトレイトを提供しており、これらを実装した構造体が前述した処理を行うことを想定しています。RequestConfigWebSocketConfigを通じて、なるべくカスタマイズ性が高くなるようにしています。

2.2. crypto-botters-*

上に書いた、RequestHandlerWebSocketHandlerを各取引所向けに実装した構造体を提供しているだけです。認証とデータのパースを行います。ファイルはlib.rsしかありません。

2.3. crypto-botters

何もしていません。 ユーザーがこのパッケージ構造を気にせず使えるよう、必要なものを1つのクレートにまとめているだけです。binancebitflyerという名前の feature をCargo.tomlで定義し、有効化されていたらcrypto-botters-binancecrypto-botters-bitflyerを依存関係に加えてre-exportしているだけです。generic-api-clientは feature の有効無効に関わらずre-exportしています。

crypto-botters の lib.rs
pub use generic_api_client::{http as http, websocket as websocket};
#[cfg(feature = "binance")]
pub use crypto_botters_binance as binance;
#[cfg(feature = "bitflyer")]
pub use crypto_botters_bitflyer as bitflyer;
// EOF

2.4. つまり

つまり言いたいのは、generic-api-clientが重要で、他は大したことはないということです。使いたい取引所がまだ実装されていなかったり、今ある実装が気に入らなかったりする場合は、自分でRequestHandlerWebSockekHandlerを実装するだけで簡単に望む機能を実現できます。

取引所の追加については、それ用のissue にコメントしてください。

3. 使い方

crypto-bottersを実際に使う例を載せ、解説を加えます。

この記事に載せていない例がexamplesディレクトリにあります。すべて単体で動作確認済です。是非参考にしてください。

binance_http_public.rs
let binance = Binance::new(None, None);
let client = Client::new();
let orderbook: serde_json::Value = client.get(
    "https://api.binance.com/api/v3/ticker/bookTicker",
    Some(&json!({ "symbol": "BTCUSDT" })),
    &binance.request_no_url(BinanceSecurity::None),
).await.expect("failed get orderbook");
println!("BTC bidPrice:\n{:?}", orderbook["bidPrice"]);

HTTPリクエストを送る際は、Client::get()などを呼びます。最後の引数にはRequestHandlerを渡します。Binance.request_no_url()RequestHandlerを実装したBinanceRequestHandlerを返すので、それの参照を渡します。BinanceSecurity::Noneは認証をしないという意味です。

no_urlとはurlを指定しない、という意味です。Binance.request()から返されるBinanceRequestHandlerはurlの先頭にhttps://api.binance.comなどを付与しますが、Binanceの場合これが何種類もあるため、Binance.request()の引数に指定しなければなりません。request_no_url()を使って回避できます。

二番目の引数の json!({ "symbol": "BTCUSDT" })はリクエストパラメータです。json!とは serde_jsonValue型という、JSONを表す型を返すマクロです。この他にも&[("symbol","BTCUSDT")]と指定したり、symbolというフィールドを持った構造体を渡す事もできます。


binance_http_private.rs
#[derive(Serialize)]
struct TradesLookupParams<'a> {
    symbol: &'a str,
    limit: u16,
}

#[derive(Deserialize)]
struct OldTrade {
    id: i64,
    price: Decimal,
    qty: Decimal,
    quote_qty: Decimal,
    time: u64,
    is_buyer_maker: bool,
    is_best_match: bool,
}

let key = env::var("BINANCE_API_KEY").expect("no API key found");
let secret = env::var("BINANCE_API_SECRET").expect("no API secret found");
let binance = Binance::new(Some(key), Some(secret));
let client = Client::new();

let trades: Vec<OldTrade> = client.get(
    "/api/v3/historicalTrades",
    Some(&TradesLookupParams { symbol: "BTCUSDT", limit: 3 }),
    &binance.request(BinanceSecurity::Key, BinanceHttpUrl::Spot),
).await.expect("failed to get trades");
println!("Trade price:\n{:?}", trades[0].price);

これは認証機能の例です。このエンドポイントは署名ではなく、APIキーを送ることが必要です(BinanceSecurity::Keyの部分)。

また、tradesの型に注目してください。自分で定義したOldTrade型のVecと指定すると、自動でレスポンスをその型に変換してくれます。面倒くさい場合は前の例のように、serde_json::Valueと指定してください。


bitflyer_websocket_private.rs
let key = env::var("BITFLYER_API_KEY").expect("no API key found");
let secret = env::var("BITFLYER_API_SECRET").expect("no API secret found");
let bitflyer = BitFlyer::new(Some(key), Some(secret));

let connection = WebSocketConnection::new(
    "/json-rpc",
    bitflyer.websocket(|message| {
        println!("{:?}", message);
    }, vec!["child_order_events"], true),
).await.expect("failed to connect websocket");

// receive messages
tokio::time::sleep(Duration::from_secs(10)).await;

// reconnect
connection.reconnect_state().request_reconnect();

// we should still see private channel messages
tokio::time::sleep(Duration::from_secs(10)).await;

これはbitFlyerのプライベートWebSocketチャンネルに購読する例です。bitFlyerのWebSocketは認証メッセージを送ってレスポンスを確認してからプライベートチャンネルの購読メッセージを送る仕様なのですが、そういうこともやってくれます。

BitFlyer.websocket()に渡しているクロージャですが、これはメッセージを受け取ったときに実行されるものです。この例では標準出力に出力しているだけです。

connection.reconnect_state().request_reconnect();では再接続をライブラリに要求しています。再接続をしたあとでも、認証は自動で行われます。再接続については後で詳しく説明します。

4. generic-api-client の構造と使い方

generic-api-client が提供しているパブリックな構造体やトレイトなどについて説明します。httpモジュールとwebsocketモジュールに分かれています。

4.1. http モジュール

HTTPリクエストを送るためのモジュールです。

4.1.1. Client

ClientはHTTPリクエストを送るための構造体です。Client::new()で構築します。メソッドは

  • request()
  • get()
  • get_no_query()
  • post()
  • post_no_body()
  • put()
  • put_no_body()
  • delete()
  • delete_no_query()

の9つです。request()以外は引数を補ってrequest()を呼んでいるだけです。GETDELTEはボディは指定できず、POSTPUTはパラメータは指定できません。指定したい場合はrequest()を直接呼んでください。

request()のシグネチャを見てみましょう。

http.rs
pub async fn request<Q, B, H>(
    &self, method: Method, url: &str, query: Option<&Q>, body: Option<B>, handler: &H,
) -> Result<H::Successful, RequestError<H::BuildError, H::Unsuccessful>>
where
    Q: Serialize + ?Sized,
    H: RequestHandler<B>,

型パラメータが3つあります。Qはクエリの型、Bはボディの型、Hはハンドラの型です。ハンドラにはRequestHandlerを実装したものを渡します。RequestHandlerについては後で詳しく説明します。

返り値はResult<H::Successful, RequestError<H::BuildError, H::Unsuccessful>>と、ハンドラの関連型(associated type)のResultになっています。これはハンドラの実装によって返り値の型を変えられるようにするためです。例えば、ハンドラにBinnaceRequestHandlerを指定した場合、返り値がErrバリアントの場合はBinanceHandlerError型になります。(実際は次に説明するRequestErrorの中に入っています。)

get()post()などに no_query や no_body がついたバージョンがあるのは、クエリやボディにNoneを指定すると型推論が働かず、何か適当な型を指定しなければならないという問題を回避するためです。Rustでもオーバーロードができるようになって欲しいですね。

4.1.2. RequestError

request()などの返り値のErrの方に入っている、エラーを表す列挙型です。
バリアントは

  • SendRequest(reqwest::Error)
  • ReceiveResponse(reqwest::Error)
  • BuildRequestError(E)
  • ResponseHandleError(R)

の4つです。SendRequestReceiveResponseの意味はそのままです。reqwestからエラーが返されたらそれをそのまま渡しています。

BuildRequestErrorは、request()がハンドラにリクエストを作らせたときに、ハンドラがエラーを返したときに使われます。中身がジェネリックのEになっていますが、これはリクエストハンドラの実装によってエラーの型を変えられるようにするためです。エラーになるのは、認証を有効にしているのにAPIキーが指定されていなかった場合などを想定しています。

ResponseHandleErrorは、request()がハンドラにレスポンスを処理させたときに、ハンドラがエラーを返したときに使われます。これも中身はジェネリックです。エラーになるのは、サーバが、例えば注文を出すのに必要なお金がないなどの理由でエラーを返してきたときなどを想定しています。

4.1.3. RequestConfig

RequestConfigはリクエストの設定をするための構造体です。ハンドラによってrequest()に渡されます。
フィールドは

  • max_try: u8
  • retry_cooldown: Duration
  • timeout: Duration
  • url_prefix: String

の4つです。timeoutはリクエストのタイムアウトでデフォルトで3秒、url_prefixはURLの先頭に付与されるもので"https://api.binance.com"などを想定しています。

残りの2つについてですが、これはrequest()の自動再試行機能の設定です。私はEC2でbotを動かしているのですが、なぜかリクエストがタイムアウトが失敗することがたまにあります。その場合には少し間を開けて再試行すると問題なく動くので、この機能を入れました。request()はリクエストの送信に失敗すると、retry_cooldownの間待ってからリクエストを作り直して再送信します。max_tryはリクエストの最大送信回数を指定します。1回目も含めての回数なので、デフォルトは1で自動再試行機能は無効化されています。

4.1.4. RequestHandler

ここまででご理解いただけたと思いますが、RequestHandlerはリクエストの様々な処理をやってくれる構造体に実装する重要なトレイトです。
関連型は

  • Successful
  • Unsuccessful
  • BuildError

の3つ、
メソッドは

  • build_request()
  • handle_response()
  • request_config()

の3つです。
また、Bという型パラメータが1つあります。

4.1.4.1. request_config()

request_config()はリクエストの設定を表すRequestConfigrequest()に渡すためのメソッドです。&self以外に引数はなく、返り値はRequestConfigです。

4.1.4.2. build_request()

build_request()request()から作りかけのreqwest::RequestBuilderを受け取って完成させるメソッドです。この中で認証に必要なヘッダ付与などを行います。シグネチャを見てみましょう。

http.rs
fn build_request(&self, builder: RequestBuilder, request_body: &Option<B>, attempt_count: u8)
    -> Result<Request, Self::BuildError>;

attempt_countはこれが何回目のリクエストかが入っています。何に使うのかはわかりませんが一応渡しています。

返り値の型はResult<Request, Self::BuildError>です。正常に認証などを行えた場合はOk(reqwest::Request)を、APIキーがないなどのエラーの場合はErr(BuildError)を指定します。BuildErrorは関連型なので自由に指定できます。request()BuildErrorRequestError::BuildRequestError(BuildError)という風にRequestErrorに入れて呼び出し元に返します。

ボディの型が&Option<B>とジェネリックになっていますが、これはrequest()に渡されるボディの型もジェネリックだからです。(request()Bという型パラメータを思い出してください。)一つのRequestHandlerで複数種類のボディに対応できるよう、Bは関連型ではなくジェネリックの型パラメータにしています。

4.1.4.3. handle_response()

handle_response()request()からレスポンスを受け取ってrequest()の呼び出し元に返す形に加工するメソッドです。
シグネチャを見てみましょう。

http.rs
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes)
    -> Result<Self::Successful, Self::Unsuccessful>;

引数の意味はそのままです。返り値はResult<Successful,Unsuccessful>です。SuccessfulUnsuccessfulもどちらも関連型なので自由に指定できます。前にも書きましたが、APIからエラーメッセージが返された場合にErr(Unsuccessful)を返すことを想定しています。
request()の呼び出し元には、返り値がOk(Successful)の場合はそのままOk(Successul)が、Err(Unsuccessful)の場合はRequestError::RequestHandleError(Unsuccessful)という風にRequestErrorに入れられたものが返されます。

4.1.4.4. 実装例

実際にRequestHandlerを実装しているBinanceRequestHandlerの実装を見てみましょう。(関数の中身は省略しています。)

crypto-botters-binance/lib.rs
pub struct BinanceRequestHandler<'a, R: DeserializeOwned> {
    api_key: Option<&'a str>,
    api_secret: Option<&'a str>,
    security: BinanceSecurity,
    base_url: BinanceHttpUrl,
    max_try: u8,
    _phantom: PhantomData<*const R>,
}

impl<'a, B, R> RequestHandler<B> for BinanceRequestHandler<'a, R>
where
    B: Serialize,
    R: DeserializeOwned,
{
    type Successful = R;
    type Unsuccessful = BinanceHandlerError;
    type BuildError = &'static str;

    fn request_config(&self) -> RequestConfig

    fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8)
        -> Result<Request, Self::BuildError>

    fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) 
        -> Result<Self::Successful, Self::Unsuccessful>
}

フィールドにAPIキーやAPIシークレット、認証の有無、URLのFQDN、RequestConfigにセットするmax_tryなどがあります。

わかりにくいのはRという型パラメータです。そのまま関連型のSuccessfulに指定されています。これはリクエストが成功したときにrequest()の呼び出し元に返される型でした。なぜこれが型パラメータになっているかというと、ユーザーがrequest()を呼び出すときに型を指定できるようにするためです。

この例を思い出してください。

binance_http_private.rs
let trades: Vec<OldTrade> = client.get(
    "/api/v3/historicalTrades",
    Some(&TradesLookupParams { symbol: "BTCUSDT", limit: 3 }),
    &binance.request(BinanceSecurity::Key, BinanceHttpUrl::Spot),
).await.expect("failed to get trades");

呼び出し元がtradesの型をVec<OldTrade>と指定することで、自動的にその型に変換されています。これはBinanceRequestHandlerhandle_response()のなかで、serde_json::from_slice()を使っているのですが、それではどうやって変換先の型を知るのでしょうか。request()からは型の情報は渡されず、Successfulという関連型に自分で指定しなければなりません。

その答えが、変換先の型をRというジェネリックの型パラメータに入れることです。つまり、この例でrequest()に渡しているハンドラの型はBinanceRequestHandler<Vec<OldTrade>>です。このハンドラによって処理されたリクエストはすべてVec<OldTrade>に変換されます。別の型に変換したいときは、別のBinanceRequestHandlerを使わなければなりません。Binance::request()BinanceRequestHandlerを取得していますが、そのシグネチャを見てみましょう。

crypto-botters-binance/lib.rs
pub fn request<R: DeserializeOwned>(&self, security: BinanceSecurity, base_url: BinanceHttpUrl)
    -> BinanceRequestHandler<R>

この関数にもRという型パラメータがあることがわかります。このようにして、ユーザーが指定した型にデシリアライズしています。

api_keyapi_secretStringではなく&'a strなのは、BinanceRequestHandlerが再利用されることが想定されておらず、Client::request()に渡したらその場で破棄されることを想定しているからです。リクエストごとにBinance::request()を呼び出してBinanceRequestHandlerを取得する設計となっているため、無駄なコピーが発生しないように参照になっています。

ちなみに、_phantomはコンパイラに怒られないようにするために必要です。これがないとRの型パラメータがフィールドの型にも関数のシグネチャにも使われていない状態となるため、

コンパイラ君
error[E0392]: parameter `R` is never used

と言われてしまいます。

4.2. websocket モジュール

WebSocket接続を行うためのモジュールです。

4.2.1. WebSocketConnection

WebSocketの接続を表す構造体です。この構造体が存在している間はWebSocketが切断されても自動で再接続されます。この構造体がdropされると接続も閉じられます。

WebSocketConnectionを構築するためには、WebSocketConnection::new()を使ってください、Client::request()と同様に、WebSocketHandlerトレイトを実装したハンドラを渡す必要があります。受信したメッセージはこのハンドラに渡されます。

4.2.1.1. 再接続

前にも少し述べましたが、WebSocketConnectionには再接続機能があります。再接続が行われるのは、

  • メッセージ受信時に何かしらのエラーが発生したとき
  • 接続を開いてからWebSocketConfigで指定された時間が経過したとき
  • ユーザーがReconnectState::request_reconnect()を呼んだとき

の3通りです。時間経過で自動で再接続する機能があるのは、例えばBinanceでは一つの接続は24時間で自動で閉じられてしまうためです。crypto-botters-binanceを使っている場合は、デフォルトで12時間で再接続されます。

再接続の方法ですが、メッセージがダブったり抜けたりしないようにするために、工夫をしています。generic-api-clientのデフォルトのignore_duplicate_during_reconnectionというオプションがfalseの場合は以下の様になっています。

generic-api-clientのデフォルトではfalseですが、crypto-botters-binancecrypto-botters-bitflyerはデフォルトでtrueに変更します。

4.2.1.2 ignore_duplicate_during_reconnection: false

websocket_reconnection_default.jpg

新しい接続を開き終わってから古い接続を切断します。

ここで重要なのが新しい接続を開く前から古い接続を閉じ終わったあとまでを囲っている、赤い枠です。この枠の中では、接続が二重に開かれているため受け取ったメッセージをそのまま使うとダブりが発生する可能性があります。そこで、以下のようなアルゴリズムでダブりをなるべく排除します

  • メッセージを受け取ったら、同じメッセージを新旧の接続のどちらが多く既に受信しているか調べる。
もしどちらも受信したことがなければメッセージは有効
ダブりである恐れがないので有効
すでに多く受信している方から受信した場合は有効
例えば新しい接続がそのメッセージを3回、古い接続が2回受信している場合に、新しい接続が同じメッセージを更にもう1回受け取った場合
受信回数が少ない方から受信した場合は無効
受信回数が少ないということは、その接続がもう片方の接続よりも遅れているということを示唆しています。その状況で、もう片方の接続が既に多く受信しているメッセージを受信したのなら、そのメッセージはもう片方の接続が既に受信しているメッセージが遅れて届いただけだと思われるので、無効として無視します。

もちろん、このやり方は確実ではありません。というか、全く同じメッセージが短時間のうちに何回も送られてくる場合に2つの接続の間で遅延が発生すると、実際に何回のメッセージが送られてきたのかを判断する方法はありません。

4.2.1.3 ignore_duplicate_during_reconnection: true

しかし、全く同じメッセージが複数回受信されない ということがわかっていれば、もっと精度を上げる事ができます。それがignore_duplicate_during_reconnection: trueです。このモードでは、以下のようにして再接続を行います。

websocket_reconnection_no_duplicate.jpg

赤い枠の始まりから緑の線の長さだけ待ち、新しい接続を開くとまた緑の線の長さだけ待ち、古い接続を切断してからも緑の線の長さだけ待ったあとに赤い枠を閉じます。

ignore_duplicate_during_reconnection: falseではダブりが発生しないように新しい接続を開いたらすぐに古い接続を閉じていましたが、ignore_duplicate_during_reconnection: trueの場合は同じメッセージは複数回受信されないとわかっているので、ダブりについて心配する必要はありません。そのため、今度は逃すメッセージをなくすことに集中できます。3回待機を挟んで赤い枠の範囲を広げ、なるべく多くのメッセージを受信することで逃すメッセージを減らしています。

この場合、赤い枠の中での処理も単純です。

  • 既に受信したメッセージはすべて無視する。

このモードでBinanceのBTCUSDTの取引をリアルタイムで受信している状況で、再接続を行ってみました。BinanceのBTCUSDTはかなりの頻度で取引があるのですが、再接続中も1件の抜けもダブりもありませんでした。

問題なく動いていると思われるため、crypto-botters-binancecrypto-botters-bitflyerではデフォルトでこちらのモードを使用しています。

4.2.1.4. その他の機能

WebSocketConnection::send_message()というメソッドを呼ぶことで、メッセージを送信することができます。

また、WebSocketConnection::reconnect_state()というメソッドを呼ぶことで、このWebSocketConnectionに対応するReconnectStateという構造体を得られます。これについては次に詳しく説明します。

4.2.2. ReconnectState

ReconnectStateは、上で説明した再接続の状況を管理するための構造体です。内部でArcを使用しているため、クローンすることができます。メソッドは2つだけです。

  • is_reconnecting()は、現在WebSocketConnectionが再接続を行なっている場合にtrueを返します。

  • request_reconnect()を呼ぶと、WebSocketConnectionに再接続をさせることができます。

4.2.3. WebSocketMessage

WebSocketMessageはWebSocketのメッセージを表す列挙型です。tungstenite::Messageを少し変えただけです。列挙型なのはテキストのメッセージ以外にもpingやpongなどがあるからです。

4.2.4. WebSocketConfig

WebSocketConfigWebSocketConnectionの設定を表す構造体で、RequestConfigのwebsocketモジュール版です。
フィールドは

  • connect_cooldown: Duration
  • refresh_after: Duration
  • url_prefix: String
  • ignore_duplicate_during_reconnection: bool
  • reconnection_wait: Duration

の5つです。

connect_cooldownは接続を行ってから、次に接続を行うまでに待つ必要のある時間です。

refresh_afterは自動再接続する時間です。generic-api-clientのデフォルトはDuration::Zeroで、時間経過による自動再接続は無効化されていますが、crypto-botters-binanceではデフォルトで12時間に設定されています。

url_prefixRequestConfigのときと同じです。"wss://stream.binance.com:9443"などを指定することを想定しています。

ignore_duplicate_during_reconnection: boolWebSocketConnectionの説明のなかで説明しました。crypto-botters-binancecrypto-botters-bitflyerでは短くno_duplicateと呼ばれています。

reconnection_waitは、WebSocketConnectionの再接続の説明のなかの緑の線の長さです。デフォルトでは300msです。

4.2.5. WebSocketHandler

最後に、WebSocketHandlerです。RequestHandlerと異なり、型パラメータや関連型はありません。

スレッド間を転送されるため、WebSockethandlerを実装する構造体は、Sendを実装していて、'staticである必要があります。

メソッドは

  • websocket_config()
  • handle_start()
  • handle_message()
  • handle_close()

の4つあります。

4.2.5.1. websocket_config()

websocket_config()RequestConfig::request_config()に対応します。引数は&selfのみ、返り値はWebSocketConfigです。

4.2.5.2. handle_start()

handle_start()は接続が開かれたときに呼ばれます。引数は&mut selfのみです。再接続したときにも呼ばれることに注意してください。

返り値はVec<WebSocketMessage>です。これは、WebSocketの接続に送られるメッセージです。bitFlyerのWebSocketは接続を開いたときに購読メッセージをおくる仕様になっているため、handle_start()が購読メッセージを返すようになっています。

4.2.5.3. handle_message()

handle_message()はメッセージを受信したときに呼ばれます。引数として&mut selfWebSocketMessageを取ります。返り値はhandle_starT()と同じくVec<WebSocketMessage>で、同じようにこのメソッドから返されたメッセージはWebSocketを通して送られます。

これはhandle_start()handle_close()に関しても同じですが、handle_message()内で 重い処理を行わないで ください。handle_message()が同時に実行されることはありません。handle_message()が返らないと、次のメッセージを渡せません。

4.2.5.4. handle_close()

これは接続が閉じられたときに呼ばれます。&mut selfの他にreconnectというbool型の引数を取ります。再接続のために接続が閉じられたときはreconnecttrue、そうでない場合、つまりWebSocketConnectionがdropされてもう再接続されることがない場合はreconnectfalseになります。

5. さいごに

ここまで読んでくれてありがとうございました。

要望・質問・提案・苦情等ある方は、GithubTwitterまでお願いします。

次に挙げるのは参考になった記事等です。

感謝します!

17
6
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6