Rust版pybotters、crypto-botters

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

https://qiita.com/advent-calendar/2022/botter

こんにちは。ねぎ草([@negi_grass](https://twitter.com/negi_grass))と申します。
[crypto-botters](https://crates.io/crates/crypto-botters)([GitHub](https://github.com/negi-grass/crypto-botters#%E6%97%A5%E6%9C%AC%E8%AA%9E))というRustのライブラリを開発しているのでその紹介をしようと思います。貢献してくれる方は、是非[issueを立て](https://github.com/negi-grass/crypto-botters/issues/new)たり[プルリク](https://github.com/negi-grass/crypto-botters/pulls)を送ったりしてください!

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

:::note alert
最新バージョンは、この記事を書いた当時のコードと**大きく異なっています**。[GitHubのREADME](https://github.com/negi-grass/crypto-botters#%E6%97%A5%E6%9C%AC%E8%AA%9E)が最新の内容です。(generic-api-clientの部分は変わっていません。)
:::

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

## 0.2. 何のライブラリか
タイトルにもある通り、完全に同じものを目指しているわけではありませんが、まちゅけんさんの[pybotters](https://github.com/MtkN1/pybotters)のRust版的なものです。（私が勝手に思ってるだけです。）できることは似ていますが、DataStoreの機能が（まだ？）なく、対応しているのは~~まだBinaneとbitFlyerだけ~~です。また、WebSocketの自動再接続に少しだけ力を入れています。

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

# 1. Rustのメリット
まずはじめに[Rust](https://www.rust-lang.org/ja)の宣伝をさせてください。（Rustを使っている方は飛ばしてください。）ここでは、Rustメリットを3つ紹介します。

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

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

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074858/0388ba9e-c97c-0d8a-2a95-b59190e4e4bf.jpeg)
`crypto-botters-binance`と`crypto-botters-bitflyer`が`generic-api-client`に依存していて、それらすべてに`crypto-botters`が依存しています。

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

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

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

## 2.3. crypto-botters
**何もしていません。** ユーザーがこのパッケージ構造を気にせず使えるよう、必要なものを1つのクレートにまとめているだけです。`binance`と`bitflyer`という名前の feature を`Cargo.toml`で定義し、有効化されていたら`crypto-botters-binance`や`crypto-botters-bitflyer`を依存関係に加えてre-exportしているだけです。`generic-api-client`は feature の有効無効に関わらずre-exportしています。
```rust: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`が重要で、他は大したことはないということです。使いたい取引所がまだ実装されていなかったり、今ある実装が気に入らなかったりする場合は、自分で`RequestHandler`や`WebSockekHandler`を実装するだけで簡単に望む機能を実現できます。

:::note
取引所の追加については、[それ用のissue](https://github.com/negi-grass/crypto-botters/issues/1) にコメントしてください。
:::

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

:::note info
この記事に載せていない例が[examplesディレクトリ](https://github.com/negi-grass/crypto-botters/tree/main/examples)にあります。すべて単体で動作確認済です。是非参考にしてください。
:::

```rust: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_json](https://crates.io/crates/serde_json) の`Value`型という、JSONを表す型を返すマクロです。この他にも`&[("symbol","BTCUSDT")]`と指定したり、`symbol`というフィールドを持った構造体を渡す事もできます。

**********

```rust: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`と指定してください。

**********

```rust: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
https://docs.rs/generic-api-client/latest/generic_api_client/http/struct.Client.html

`Client`はHTTPリクエストを送るための構造体です。`Client::new()`で構築します。メソッドは
- `request()`
- `get()`
- `get_no_query()`
- `post()`
- `post_no_body()`
- `put()`
- `put_no_body()`
- `delete()`
- `delete_no_query()`

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

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

```rust: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
https://docs.rs/generic-api-client/latest/generic_api_client/http/enum.RequestError.html

`request()`などの返り値の`Err`の方に入っている、エラーを表す列挙型です。
バリアントは
- `SendRequest(reqwest::Error)`
- `ReceiveResponse(reqwest::Error)`
- `BuildRequestError(E)`
- `ResponseHandleError(R)`

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

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

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

### 4.1.3. RequestConfig
https://docs.rs/generic-api-client/latest/generic_api_client/http/struct.RequestConfig.html

`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
https://docs.rs/generic-api-client/latest/generic_api_client/http/trait.RequestHandler.html

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

の3つ、
メソッドは
- `build_request()`
- `handle_response()`
- `request_config()`

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

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

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

```rust: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()`は`BuildError`を`RequestError::BuildRequestError(BuildError)`という風に`RequestError`に入れて呼び出し元に返します。

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

#### 4.1.4.3. handle_response()
`handle_response()`は`request()`からレスポンスを受け取って`request()`の呼び出し元に返す形に加工するメソッドです。
シグネチャを見てみましょう。
```rust:http.rs
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes)
    -> Result<Self::Successful, Self::Unsuccessful>;
```
引数の意味はそのままです。返り値は`Result<Successful,Unsuccessful>`です。`Successful`も`Unsuccessful`もどちらも関連型なので自由に指定できます。前にも書きましたが、APIからエラーメッセージが返された場合に`Err(Unsuccessful)`を返すことを想定しています。
`request()`の呼び出し元には、返り値が`Ok(Successful)`の場合はそのまま`Ok(Successul)`が、`Err(Unsuccessful)`の場合は`RequestError::RequestHandleError(Unsuccessful)`という風に`RequestError`に入れられたものが返されます。

#### 4.1.4.4. 実装例
実際に`RequestHandler`を実装している`BinanceRequestHandler`の実装を見てみましょう。（関数の中身は省略しています。）
```rust: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()`を呼び出すときに型を指定できるようにするためです。

この例を思い出してください。
```rust: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>`と指定することで、自動的にその型に変換されています。これは`BinanceRequestHandler`の`handle_response()`のなかで、`serde_json::from_slice()`を使っているのですが、それではどうやって変換先の型を知るのでしょうか。`request()`からは型の情報は渡されず、`Successful`という関連型に自分で指定しなければなりません。

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

```rust:crypto-botters-binance/lib.rs
pub fn request<R: DeserializeOwned>(&self, security: BinanceSecurity, base_url: BinanceHttpUrl)
    -> BinanceRequestHandler<R>
```
この関数にも`R`という型パラメータがあることがわかります。このようにして、ユーザーが指定した型にデシリアライズしています。

`api_key`や`api_secret`が`String`ではなく`&'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
https://docs.rs/generic-api-client/latest/generic_api_client/websocket/struct.WebSocketConnection.html

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`の場合は以下の様になっています。

:::note warn
`generic-api-client`のデフォルトでは`false`ですが、`crypto-botters-binance`や`crypto-botters-bitflyer`はデフォルトで`true`に変更します。
:::

#### 4.2.1.2 ignore_duplicate_during_reconnection: false

![websocket_reconnection_default.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074858/f7ec3382-19e9-0295-cc8a-a2e79ad31460.jpeg)

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

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

- メッセージを受け取ったら、同じメッセージを新旧の接続のどちらが多く既に受信しているか調べる。

<dl>

<dt>もしどちらも受信したことがなければメッセージは有効</dt>
<dd>ダブりである恐れがないので有効</dd>

<dt>すでに多く受信している方から受信した場合は有効</dt>
<dd>例えば新しい接続がそのメッセージを3回、古い接続が2回受信している場合に、新しい接続が同じメッセージを更にもう1回受け取った場合</dd>

<dt>受信回数が少ない方から受信した場合は無効</dd>
<dd>受信回数が少ないということは、その接続がもう片方の接続よりも遅れているということを示唆しています。その状況で、もう片方の接続が既に多く受信しているメッセージを受信したのなら、そのメッセージはもう片方の接続が既に受信しているメッセージが遅れて届いただけだと思われるので、無効として無視します。</dd>

</dl>

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

#### 4.2.1.3 ignore_duplicate_during_reconnection: true

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

![websocket_reconnection_no_duplicate.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074858/e14e7661-0aca-f88c-d194-62584871dc8a.jpeg)

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

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

この場合、<font color="red">赤い枠</font>の中での処理も単純です。

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

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

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

####  4.2.1.4. その他の機能
`WebSocketConnection::send_message()`というメソッドを呼ぶことで、メッセージを送信することができます。

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

### 4.2.2. ReconnectState
https://docs.rs/generic-api-client/latest/generic_api_client/websocket/struct.ReconnectState.html

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

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

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

### 4.2.3. WebSocketMessage
https://docs.rs/generic-api-client/latest/generic_api_client/websocket/enum.WebSocketMessage.html

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

### 4.2.4. WebSocketConfig
https://docs.rs/generic-api-client/latest/generic_api_client/websocket/struct.WebSocketConfig.html

`WebSocketConfig`は`WebSocketConnection`の設定を表す構造体で、`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_prefix`は`RequestConfig`のときと同じです。`"wss://stream.binance.com:9443"`などを指定することを想定しています。

`ignore_duplicate_during_reconnection: bool` は`WebSocketConnection`の説明のなかで説明しました。`crypto-botters-binance`や`crypto-botters-bitflyer`では短く`no_duplicate`と呼ばれています。

`reconnection_wait`は、`WebSocketConnection`の再接続の説明のなかの<font color="green">緑の線</font>の長さです。デフォルトでは300msです。

### 4.2.5. WebSocketHandler
https://docs.rs/generic-api-client/latest/generic_api_client/websocket/trait.WebSocketHandler.html

最後に、`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 self`と`WebSocketMessage`を取ります。返り値は`handle_starT()`と同じく`Vec<WebSocketMessage>`で、同じようにこのメソッドから返されたメッセージはWebSocketを通して送られます。

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

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

# 5. さいごに
ここまで読んでくれてありがとうございました。

要望・質問・提案・苦情等ある方は、[Github](https://github.com/negi-grass/crypto-botters)か[Twitter](https://twitter.com/negi_grass)までお願いします。

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

https://qiita.com/pe3/items/aa621bd1ed732b501cf4

https://github.com/MtkN1/pybotters

感謝します！