QuickFIX/Jのサンプルに対してRustでリクエストを投げてみる

QuickFIX/Jとは

QuickFIX/JはFIXエンジンの実装になります。FIXエンジン自体が聞きなれないと思いますが、FIXエンジンは電子取引で使われるプロトコルであるFIXプロトコルの実装になります。FIXエンジンについては以下のリンクが参考になります。
https://javarevisited.blogspot.com/2012/01/what-is-fix-engine-in-fix-protocol.html

QuickFIX/Jの公式ページによると、もともとC++で実装されたQuickFIXをJavaで実装しなおしたもののようです。

QuickFIX/Jの実装は以下で確認できます。
github.com

今回はFIXエンジンとRustの学習のためQuickFIX/Jのソースにサンプルに含まれるexecutorbanzaiの内、executorはそのまま使ってリクエストを投げるbanzaiをRustの自前の実装に変えたいと思います。

とりあえず、LogonとOrder(single)のリクエストまでは実装できました。
github.com

Logon、Order(Single)のリクエストを飛ばすと以下のようになりました。

input method: H(Hello), O(Order)
H
send hello
request: "8=FIX.4.4\u{1}9=61\u{1}35=A\u{1}34=5\u{1}49=BANZAI\u{1}52=20200628-06:52:26\u{1}56=EXEC\u{1}98=0\u{1}108=30\u{1}10=022\u{1}"
"BeginString": "FIX.4.4"
"BodyLength": "61"
"MsgType": "A"
"MsgSeqNum": "5"
"SenderCmpId": "BANZAI"
"SendingTime": "20200628-06:52:26"
"TargetCmpID": "EXEC"
"EncryptMethod": "0"
"HeartBeatInterval": "30"
"Checksum": "022"

response: "8=FIX.4.4\u{1}9=66\u{1}35=A\u{1}34=84\u{1}49=EXEC\u{1}52=20200628-06:52:26.355\u{1}56=BANZAI\u{1}98=0\u{1}108=30\u{1}10=029\u{1}"
read: "8=FIX.4.4\u{1}9=66\u{1}10=029\u{1}34=84\u{1}35=A\u{1}49=EXEC\u{1}52=20200628-06:52:26.355\u{1}56=BANZAI\u{1}98=0\u{1}108=30\u{1}"
"BeginString": "FIX.4.4"
"BodyLength": "66"
"Checksum": "029"
"MsgSeqNum": "84"
"MsgType": "A"
"SenderCmpId": "EXEC"
"SendingTime": "20200628-06:52:26.355"
"TargetCmpID": "BANZAI"
"EncryptMethod": "0"
"HeartBeatInterval": "30"

input method: H(Hello), O(Order)
O
send order
request: "8=FIX.4.4\u{1}9=118\u{1}35=D\u{1}11=1593327148053\u{1}21=1\u{1}34=1\u{1}38=34\u{1}40=1\u{1}49=BANZAI\u{1}52=20200628-06:52:28\u{1}54=1\u{1}55=A\u{1}56=EXEC\u{1}59=0\u{1}60=20200628-0
6:52:28\u{1}10=214\u{1}"
"BeginString": "FIX.4.4"
"BodyLength": "118"
"MsgType": "D"
"ClOrdID": "1593327148053"
"ClOrdID": "1"
"MsgSeqNum": "1"
"OrderQty": "34"
"OrderType": "1"
"SenderCmpId": "BANZAI"
"SendingTime": "20200628-06:52:28"
"Side": "1"
"Symbol": "A"
"TargetCmpID": "EXEC"
"TimeInForce": "0"
"TransactTime": "20200628-06:52:28"
"Checksum": "214"

response: "8=FIX.4.4\u{1}9=63\u{1}35=2\u{1}34=85\u{1}49=EXEC\u{1}52=20200628-06:52:26.356\u{1}56=BANZAI\u{1}7=2\u{1}16=0\u{1}10=112\u{1}"
read: "7=2\u{1}8=FIX.4.4\u{1}9=63\u{1}10=112\u{1}16=0\u{1}34=85\u{1}35=2\u{1}49=EXEC\u{1}52=20200628-06:52:26.356\u{1}56=BANZAI\u{1}"
"": "2"
"BeginString": "FIX.4.4"
"BodyLength": "63"
"Checksum": "112"
"": "0"
"MsgSeqNum": "85"
"MsgType": "2"
"SenderCmpId": "EXEC"
"SendingTime": "20200628-06:52:26.356"
"TargetCmpID": "BANZAI"

Logonのリクエストは以下のようになっています。

8=FIX.4.4\u{1}9=61\u{1}35=A\u{1}34=5\u{1}49=BANZAI\u{1}52=20200628-06:52:26\u{1}56=EXEC\u{1}98=0\u{1}108=30\u{1}10=022\u{1}

各フィールドの区切り文字の文字コードは1になっているのですが\u{1}と表示されています。 こちらの各フィールドと値が分かるように出力したのが以下になります。

"BeginString": "FIX.4.4"
"BodyLength": "61"
"MsgType": "A"
"MsgSeqNum": "5"
"SenderCmpId": "BANZAI"
"SendingTime": "20200628-06:52:26"
"TargetCmpID": "EXEC"
"EncryptMethod": "0"
"HeartBeatInterval": "30"
"Checksum": "022"

各フィールドについて8=FIX4.4の部分はExecution Report になりますがFIXエンジンのプロトコルが入るようです。
9=61の部分はBodyLengthになるのですが、Begin StringとMsgType、CheckSumを含まない部分のバイトサイズになっているようでRustでは以下のように実装しました。

        let mut result = String::from("");
        let mut msgtype = String::from("");
        let mut result2 = String::from("");
        for field in self.get_fields() {
            if field.tag == FieldKey::begin_string() {
                result.push_str(&field.to_string());
            } else if field.tag == FieldKey::msg_type() {
                msgtype.push_str(&field.to_string());
            } else if field.tag == FieldKey::checksum() {
            } else {
                result2.push_str(&field.to_string());
            }
        }
        result.push_str(
            &Field::new(
                FieldKey::body_length(),
                ((result2.len() + msgtype.len()) as i32).to_string(),
            )
                .to_string(),
        );

35=Aの部分はMsgTypeになりAはLogOneになります。
34=5の部分はMsgSeqNumになります。以下がリクエストとレスポンスになるのですがレスポンスを見ると84, 85となっているので各クライアント毎でカウントアップすればよさそうに見えます。自作の実装のRustでは5, 1となっているのでちゃんと実装されていません。

request: "8=FIX.4.4\u{1}9=61\u{1}35=A\u{1}34=5\u{1}49=BANZAI\u{1}52=20200628-06:52:26\u{1}56=EXEC\u{1}98=0\u{1}108=30\u{1}10=022\u{1}"
response: "8=FIX.4.4\u{1}9=66\u{1}35=A\u{1}34=84\u{1}49=EXEC\u{1}52=20200628-06:52:26.355\u{1}56=BANZAI\u{1}98=0\u{1}108=30\u{1}10=029\u{1}"
request: "8=FIX.4.4\u{1}9=118\u{1}35=D\u{1}11=1593327148053\u{1}21=1\u{1}34=1\u{1}38=34\u{1}40=1\u{1}49=BANZAI\u{1}52=20200628-06:52:28\u{1}54=1\u{1}55=A\u{1}56=EXEC\u{1}59=0\u{1}60=20200628-0
6:52:28\u{1}10=214\u{1}"
response: "8=FIX.4.4\u{1}9=63\u{1}35=2\u{1}34=85\u{1}49=EXEC\u{1}52=20200628-06:52:26.356\u{1}56=BANZAI\u{1}7=2\u{1}16=0\u{1}10=112\u{1}"

49=BANZAIの部分はSenderCompIDになりまして、サンプルの実装でBANZAIが入っていたので同じものを入れています。
52=20200628-06:52:26の部分はSendingTimeになります、とりあずミリ秒まで含めなくても動いていましたが元のサンプルだとミリ秒まで含めていたのでそうしたほうが良さそうです。
56=EXECの部分はTargetCompIDになります。
98=0の部分はEncryptMethodになり、0なので暗号化なしになります。
108=30の部分はHeartBeatになり、30秒間隔でのハートビートの送信になります。 10=022はCheckSumになり、Rustでは以下のように実装しました。

        for b in result.as_bytes() {
            check += *b as i32;
            check %= 256;
        }
        let checksum = Field::new(FieldKey::checksum(), format!("{:03}", check));

FIXエンジンではTCP接続を継続していて、リクエストを投げてからレスポンスを待つではなくレスポンスは待ち続けておいて、リクエストを投げているのであればどのリクエストに対するレスポンスなのかを区別できるような作りにしたほうが良さそうです。