注文は状態ではなくイベントの列だ
一言でいうと
注文の最終状態は、どこかに保存されたフィールドではなく、複数のイベントを順に畳み込んで作った結果です。畳み込みの規則と順序が決まっていなければ、同じログから異なる答えが出ます。
なぜ必要なのか
証券会社のサポートシステムに入ると、ほぼ例外なく、この一文から始まります。「約定通知は受け取ったのに、画面では取消になっています」。
最初にこれを聞くと、画面が間違っているか、DBの更新が遅れたのだと思います。そこでordersテーブルを開いてstatus列を見ます。そこにはcancelledと書かれています。次に何をすればよいかわからなければ、調査はここで止まります。
止まる理由は、注文の状態を1つの値だと信じていたからです。実際には、次のような形をしています。
09:31:04.118 new 수량 300, 지정가 71,500
09:31:04.180 ack 거래소가 접수를 확인
09:44:12.007 partial_fill 150주 체결, 누적 150
09:47:55.310 cancel_req 잔량 취소 요청
09:47:55.402 fill 150주 체결, 누적 300
09:47:55.408 cancelled 잔량 취소 확정
このコードブロックの韓国語コメントは、6行の内容が、数量300・指値71,500での新規、取引所が受付を確認、150株の約定(累計150)、残数量の取消要求、150株の約定(累計300)、残数量の取消確定であることを述べています。
この6行から、「この注文は何なのか」を取り出さなければなりません。最後のイベントだけを見れば取消です。約定数量だけを見れば全量約定です。どちらもこのログにある事実であり、どちらを状態と呼ぶかは、自分たちが決める規則です。規則を明示的に決めていないシステムでは、コードの複数の場所がそれぞれ異なる規則を使っており、そのため、画面と通知メールと清算バッチが互いに異なる答えを出します。
どう動くのか
イベントを畳み込む関数は、おおよそ次のような形をしています。ここで重要なのはコードではなく、各行がどんな判断を含んでいるかです。
new 주문 수량이 정해진다. 상태는 '접수'
ack 거래소가 받았다. 상태는 '유효'
reject 거절. 여기서 끝난다
replace 주문 수량이 바뀐다. 상태는 그대로
cancel_req 아무것도 바꾸지 않는다 ← 여기가 첫 번째 함정
cancelled 상태는 '취소'
fill 계열 누적 체결에 더하고, 주문 수량에 닿으면 '체결'
このコードブロックの韓国語コメントは、イベントごとの規則として、newは注文数量を決めて状態を受付にし、ackは取引所が受け取って状態を有効にし、rejectは拒否でそこで終わり、replaceは注文数量が変わるが状態はそのまま、cancel_reqは何も変えず(最初の落とし穴)、cancelledは状態を取消にし、fill系は累計約定に足して注文数量に達したら約定にする、という意味です。
cancel_reqが状態を変えないことが核心です。取消を要求したことと、取消されたことは別の事実です。要求は自分たちが送ったもので、確定は取引所が出したものです。要求の時点で状態をcancelledに変える実装が実際によくありますが、そうすると、要求のあとに約定が付いた注文が取消のまま残ります。顧客は株を受け取ったのに、こちらの帳簿にはありません。
2つ目の落とし穴は、一部約定が何回も来ることです。300株の注文が150 + 100 + 50の3回に分かれて約定するのは、異常なことではなく基本です。各約定は自分の分の数量だけを含み、累計はこちらで数えなければなりません。取引所がcum_qtyを一緒に渡すこともありますが、その値がこちらで数えた値と違えば、それ自体が事故のサインです。
3つ目の落とし穴は、同じ約定が2回来ることです。セッションが切れてまたつながったり、再送を要求したりすると、すでに受け取った約定が再び来ます。そのため、約定ごとに一意のexec_idが付いており、それで除外するのは受け取る側の責任です。除外しなければ、累計数量が膨らみます。注文数量を超えたものは目立ちますが、超えていないものは、何の警報もなく間違ったまま残ります。
現場での姿
ある証券会社で、夜間の清算バッチが毎日明け方に失敗していました。ログには「約定数量が注文数量を超過」というメッセージが6件あり、担当者は毎朝その6件を手で直して入れていました。6か月目でした。
原因は、再送された約定を除外していなかったことでした。そして、本当の問題はその6件ではありませんでした。同じ原因で数量が膨らんだ注文が20数件さらにあったのですが、それらは注文数量を超えていなかったので、何のメッセージも残しませんでした。毎朝直していた6件は氷山の一角で、下の部分は、6か月間、誰も数えていませんでした。
もう1つよく出会う姿は、状態フィールドとイベントログが分かれたまま何年も続くことです。状態フィールドはリアルタイム処理器が更新し、イベントログは元のまま積み上がりますが、処理器が一度でもイベントを取りこぼすと、その注文の状態フィールドは永遠に間違います。イベントは再び流れてこず、状態フィールドを再計算する手順は、たいていありません。そのため、調査のときは状態フィールドを信じず、イベントから作り直してみることが最初の手です。
その次のラボですること
まず次の理論で時計と順序を見てから、ラボに進みます。
注文900件のイベントログを自分で作り、そのログだけを使って、注文ごとの最終状態を再構成します。そのあと、OMSが持っている状態フィールドと突き合わせて、ずれた注文を探します。状態の文字列だけを比較したときと、約定数量まで比較したときで出る数字が異なり、その差が、今回の事故の本体です。