「今日」がいつなのかはシステムごとに違う
一言でいうと
取引日は暦日ではなく、セッションは市場ごとに違い、サマータイムのある市場では、同じ現地時刻が2回出てきたり、まったく存在しなかったりします。この3つを固定オフセットでひとまとめにすると、1年に2回、切り替えの週末ごとに、静かに間違います。
なぜ必要なのか
時刻を扱うコードは、たいてい2回書かれます。最初に書いたときはうまく動き、切り替えの週末に1回直されます。その間の数か月間、誰も問題を知らない理由は、間違った値が例外を投げず、ただ1時間ずれたまま保存されるからです。
資本市場では、このずれがすぐにお金とレポートに届きます。締めバッチが1時間早く回ると、その間の約定が翌日に回され、突合は、ずれたまま合わされます。そして、市場ごとに切り替えの日付が違います。米国市場がすでに夏時間に移ったあとも、欧州市場がまだ冬時間である数週間が毎年あり、その数週間は、2つの市場の引けの間隔が、普段と違います。この事実をシステムが知らなくてよい理由はありません。
どう動くのか
第一に、オフセットではなく規則で変換します。現地時刻をUTCに変えることは、「9時間引く」ではありません。その地域がその日付にどんな規則の下にあったかを見る必要があります。その規則の原本はIANAタイムゾーンデータベースで、Pythonではzoneinfoがそれを読みます。このモジュールが標準に入った背景は、PEP 615に書かれています。固定オフセットをコードに埋め込むと、切り替え日以降のすべての記録が、1時間ずつずれます。
第二に、切り替え日には、壁時計が成り立たない瞬間があります。春に時計を進める市場では、飛ばされた1時間がまるごと存在しません。その時刻が書かれた記録は、現地時刻として読めず、「とりあえず変換して保存」すると、静かに1時間後の値になります。秋に戻す市場では、同じ壁時計の値が2回通るので、その時刻だけでは、2つの瞬間のどちらかを決められません。Pythonは、この2番目の場合をfold属性で区別しますが、どちらを使うかは、業務が決めるべき規則であって、ライブラリが決めてくれるものではありません。
第三に、セッションとカレンダーは別のデータです。通常取引の前後に開場前と開場後の区間があり、半日取引があり、休場日があります。これをコードに定数として埋め込むと、年が変わるたびにデプロイが必要になります。カレンダーをデータとして置けば、そのデータ自体を検証できるという利点も生まれます。半日取引の終了が通常の終了より遅く書かれていたり、同じ日が休場日でありながら、半日取引としても書かれていたりすることは、実際に起きます。
第四に、取引日はラベルです。午前0時を越える夜間取引で、午前1時の約定は、暦では今日ですが、取引日としては昨日です。このラベルを決めないと、同じ約定がどの日の集計に入るかがシステムごとに変わり、突合は永遠に合いません。表記自体の規則はRFC 3339が定めていますが、取引日を何とみなすかは、市場の規則です。
現場での姿
最もよく見る事故は、切り替えの週末の直後の締めバッチです。ある市場の引けをUTCで固定しておくと、切り替えのあとは、その市場がまだ開いている時刻にバッチが回ります。逆に、現地時刻だけで設定しておくと、ほかの市場のバッチと同じ瞬間に重なり、2つの作業が同じリソースを巡ってぶつかります。実際に、2つの市場の引けが、夏時間の間、ちょうど同じUTC時刻に置かれる区間があり、その数週間だけ、バッチが遅れます。
2番目によくあるのは、オフセットのない時刻文字列です。ログと電文に2025-03-30 01:30:00だけが書かれていると、それがどのタイムゾーンなのかは、文書から持ってくる必要があります。そして、その時刻が、そのタイムゾーンに存在しないこともありうるところまで、扱う必要があります。
3番目は、カレンダーがコードの中にある場合です。休場日と半日取引を定数として埋め込んでおくと、年が変わるたびにデプロイが必要になり、急いで直しているうちに、その値がどこから来たのか、誰もわからなくなります。カレンダーをデータとして外に出せば、そのときから、カレンダー自体を検証できます。同じ日が休場日でありながら半日取引としても書かれていたり、半日取引の終了が通常の終了より遅く書かれていたりすることは、実際に起きますし、そうした行1つが、その日1日の集計をまるごと変えます。この検証は数行で済みますが、カレンダーがコードの中にあると、その数行を書く場所がありません。
そして、これらすべての処理に、共通の原則が1つあります。時刻は、受け取ったらすぐに1回だけ解釈し、その後は絶対時刻だけで流します。現地時刻の文字列をそのまま持ち歩き、必要なたびに解釈すると、同じ値が、システムの層ごとに違って読まれます。画面に表示するときだけ、現地時刻に変換し直せばよく、そのときは、どの市場の現地時刻かが、その画面の文脈で決まります。
次のラボですること
4つの市場のセッション定義とカレンダー、30件の約定を作り、タイムゾーンの規則でUTCに変換します。固定オフセットで変換した結果と比べてずれた日を探し、切り替え日の存在しない時刻と2回ある時刻を分けたうえで、セッション区間と取引日ラベルを付けます。最後に、市場ごとの引けをUTCにまとめて、バッチウィンドウが重なる日を探し、カレンダー自体の矛盾を2つ見つけ出します。