GPI開発日誌 Vol.4|ドル円データを毎週収集する運用・監視設計

目次

はじめに

こんにちは。

GadgetStreemでは現在、ガジェット価格の上昇圧力を一つの数字で確認できる独自指数、

GPI(Gadget Pressure Index/ガジェット価格圧力指数)

を開発しています。

これまでの開発日誌では、次の段階まで進みました。

Vol.3までで、

Federal Reserve H.10
        ↓
USD/JPY Collector
        ↓
データ検証
        ↓
PostgreSQLへ保存

という最初のデータパイプラインが形になりました。

しかし、市場データを一度取得できたからといって、そのままGPIを毎週公開できるわけではありません。

次に必要なのは、

Collectorを継続的に動かし、正常に取得できなかった場合にも気付ける仕組み

です。

今回のVol.4では、USD/JPY Collectorを「動くプログラム」から「継続運用できるシステム」へ進化させるための設計をご紹介します。


一度成功することと、毎週成功することは違う

開発中にコマンドを実行し、ドル円データを取得できたとします。

取得成功
保存成功

これだけなら、Collectorの基本機能は動いています。

しかし、実際の運用では次のような問題が起こります。

  • データ提供元へ接続できない
  • 通信が途中で切れる
  • XMLやCSVの形式が変わる
  • 最新週のデータがまだ公開されていない
  • 欠損値しか含まれていない
  • データベースへ接続できない
  • Collector自体は終了したが、一件も保存されていない
  • スケジュールされた処理が実行されなかった

つまり、

エラーが出なかった
=正しい最新データを取得できた

とは限りません。

GPIを継続公開するには、単なる成功・失敗だけでなく、

  • いつ実行したのか
  • どの期間を取得したのか
  • 何件追加したのか
  • 最新の観測日はいつか
  • 欠損がなかったか
  • 前回成功したのはいつか

まで管理する必要があります。


H.10の更新頻度に合わせる

USD/JPY Collectorが利用するFederal Reserve BoardのH.10は、リアルタイムの為替配信ではありません。

二国間為替レートは、通常、前営業週の金曜日までのデータが米国東部時間の月曜日16時15分に更新されます。
月曜日が米国の祝日の場合は、次の営業日に公開されます。

そのため、GPI側でも秒単位や分単位で何度も取得する必要はありません。

初期運用では、

Federal Reserveによる週次更新
        ↓
一定時間を空けてCollectorを実行
        ↓
新しい観測値を確認
        ↓
GPIの週次計算へ利用

という流れにします。

提供元の公表時刻直後は、更新処理やアクセス集中が発生する可能性もあります。

そのため、公開予定時刻ぴったりではなく、ある程度余裕を持って取得する方が安全です。


「観測日」と「取得日」を分ける

市場データを扱ううえで重要なのが、日付を一つだけにしないことです。

GadgetStreem Intelligenceでは、少なくとも次の日時を分けて管理します。

日時意味
観測日時為替レートが示す対象日時
公表日時提供元がデータを公開した日時
取得日時GSIがデータを取得した日時
保存日時PostgreSQLへ記録した日時

例えば、金曜日の為替データが翌週月曜日に公開され、GSIが火曜日に取得した場合、

観測日:金曜日
公表日:月曜日
取得日:火曜日

となります。

これらをすべて「火曜日のドル円」として保存してしまうと、GPIの計算期間がずれてしまいます。

そのため、GPIではデータが何を示す日時なのかを最優先で管理します。


データの鮮度を判定する

Collectorが正常に動いていても、取得できた値が古ければ、最新の市場状況を表しているとは限りません。

そこで、観測値へ鮮度の状態を付けます。

初期案は次のとおりです。

状態意味
Fresh想定どおり最新データを取得できている
Delayed提供元の更新が通常より遅れている
Stale前回値から一定期間更新されていない
Unavailable利用可能な観測値が存在しない
Invalid形式・単位・内容に問題がある

重要なのは、古い値を最新値のように見せないことです。

前回値を暫定利用する場合でも、

stale = true

のような状態を付け、公開画面にも

前回取得値を使用しています

と表示します。


Collectorの実行履歴を残す

観測値だけでなく、Collectorを実行した履歴も保存します。

一回の実行を表すCollection Runには、次のような情報を記録します。

run_id
source_id
開始日時
終了日時
対象期間
処理結果
新規追加件数
変更なし件数
Revision追加件数
スキップ件数
失敗件数
エラー概要

例えば、次のような結果です。

Status:Success
Inserted:5
Unchanged:5
Revised:0
Skipped:2
Failed:0

これにより、

Collectorは終了したが、実際には何件処理したのか

を確認できます。

一件も保存されていない場合でも、それが

  • 新しいデータがなかった
  • すべて登録済みだった
  • 欠損値しかなかった
  • 解析に失敗した

のどれなのかを区別できます。


成功・警告・失敗を分ける

Collectorの結果を、単純な成功・失敗の二択にはしません。

初期のステータス案は次のとおりです。

Success

必要なデータを正常に取得・検証・保存できた状態です。

Success with warnings

処理自体は完了したものの、次のような注意事項がある状態です。

  • 一部に欠損値があった
  • 想定より観測件数が少なかった
  • 新しい観測値がなかった
  • データの公開が遅れている
  • 前回値と比べて極端な変化があった

Failed

Collectorを正常に完了できなかった状態です。

  • HTTP接続エラー
  • タイムアウト
  • ZIP展開失敗
  • XML解析失敗
  • 対象系列が見つからない
  • DB保存失敗

Partial failure

複数件のうち、一部だけ処理できなかった状態です。

GPIでは部分失敗を無視せず、どの観測値が失敗したかを記録します。


エラーは種類ごとに分ける

すべてのエラーを

取得に失敗しました

とだけ記録しても、原因を特定できません。

そこで、エラーを役割ごとに分けます。

NetworkError
HttpError
TimeoutError
ArchiveError
ParseError
ValidationError
PersistenceError
ConfigurationError

例えば、HTTP 503で提供元が一時的に利用できない場合と、XML内から日本円の系列を見つけられない場合では、対応方法が異なります。

  • 一時的な通信障害:時間を空けて再試行
  • XML構造の変更:コードと仕様の確認が必要
  • 不正な数値:データを隔離して人間が確認
  • DB障害:保存処理をロールバック

エラーの種類を分けることで、無意味な再試行や、誤ったデータ保存を防ぎます。


何でも再試行すればよいわけではない

通信エラーが発生した場合は、再試行が有効です。

ただし、すぐに何十回もアクセスすると、提供元へ負荷をかける可能性があります。

そこで、再試行は回数を制限し、待ち時間を少しずつ長くします。

概念的には次のような流れです。

1回目:通常実行
失敗
↓
少し待って2回目
失敗
↓
さらに待って3回目
失敗
↓
処理を停止して通知

一方で、

  • XMLの構造が変わった
  • 必須の系列が存在しない
  • 単位が想定と異なる
  • 設定が不正

といった問題は、何度実行しても直らない可能性が高いため、自動再試行の対象にしません。

再試行できるエラーと、人間の確認が必要なエラーを分ける

ことが重要です。


データベースへの保存は中途半端にしない

10件の観測値を保存している途中でエラーが発生した場合、

最初の6件だけ保存
残り4件は未保存

となると、その週のデータが不完全になります。

そこで、関連する保存処理はPostgreSQLのトランザクション内で実行します。

処理全体が成功した場合だけ確定し、途中で問題が発生した場合は元の状態へ戻します。

また、同一キーの挿入競合を安全に扱う仕組みとして、PostgreSQLにはINSERT ... ON CONFLICTが用意されています。ON CONFLICT DO UPDATEは、競合時の挿入または更新を原子的に実行できます。

ただし、GSIでは過去値を無言で上書きしません。

同じ値ならunchanged、公式値が変わっていれば新しいRevisionを追加するという独自ルールを優先します。


自動実行と手動実行の両方を残す

Collectorは最終的に自動実行します。

しかし、自動実行だけにすると、障害調査や過去データの再取得が難しくなります。

そのため、次の両方を残します。

自動実行

決められた曜日・時刻に、最新データを取得します。

手動実行

必要に応じて、開発者がCLIから実行します。

gsi collect-usd-jpy

将来的には対象期間を指定できるようにします。

gsi collect-usd-jpy \
  --from 2026-07-01 \
  --to 2026-07-31

これにより、

  • 自動取得に失敗した期間の再実行
  • 過去データの追加取得
  • 新しいCollectorの動作確認
  • 障害復旧後の再処理

が可能になります。


GitHub Actionsのスケジュールは「正確な時刻」ではない

初期段階では、GitHub Actionsを使って定期実行する案もあります。

GitHub Actionsでは、cron形式のscheduleイベントでワークフローを自動実行できます。

ただし、GitHub公式ドキュメントでは、負荷が高い時間帯にはスケジュール実行が遅延し、状況によってはキューから削除される可能性もあると説明されています。また、スケジュールワークフローはデフォルトブランチ上のファイルだけが対象です。

そのため、GPIではGitHub Actionsを

正確な時刻を保証する市場データ配信基盤

としては扱いません。

開発・検証段階では十分に活用できますが、本番運用では、

  • 遅れても問題のない余裕ある時刻に設定
  • 実行されなかったことを検知
  • 手動再実行を可能にする
  • 必要に応じて専用スケジューラーへ移行

という方針にします。


毎時0分を避ける

GitHubは、毎時ちょうどの時間帯はスケジュール実行が集中し、遅延しやすいと案内しています。

そのため、例えば、

毎週火曜日 6:00

ではなく、

毎週火曜日 6:17

のように、実行が集中しにくい時刻を選びます。

また、GitHub ActionsのcronはUTC基準なので、JST(日本時間)との時差も考慮する必要があります。

日本時間の実行時刻を決める場合は、

JST = UTC+9時間

として設定します。


Dockerの再起動だけでは解決しない

コンテナが異常終了した場合、Dockerには再起動ポリシーがあります。

on-failureでは異常終了時だけ再起動し、alwaysunless-stoppedでは条件に応じて継続的に再起動できます。

ただし、CollectorはAPIサーバーのように常時起動するプロセスではなく、

開始
↓
取得
↓
保存
↓
終了

という一回限りのジョブです。

そのため、単純にコンテナを再起動し続けると、設定ミスやデータ形式変更でも無限に失敗を繰り返す恐れがあります。

Collectorでは、コンテナ再起動よりも、

  • ジョブ単位の再試行
  • 最大試行回数
  • 実行履歴
  • エラー通知
  • 手動再実行

を重視します。


正常に動いているかを監視する

APIにはすでに、

/health/live
/health/ready

というヘルスチェックがあります。

今後はCollectorにも、運用状態を確認できる情報を追加します。

例えば、

最終実行日時
最終成功日時
最新観測日
現在のデータ鮮度
直近のエラー
連続失敗回数

です。

将来的な管理画面では、次のような表示を想定しています。

USD/JPY

Status:Fresh
Latest observation:2026-07-17
Last successful collection:2026-07-21 06:17 JST
Consecutive failures:0

問題がある場合は、

Status:Stale
Latest observation:2026-07-10
Last error:No new observation was found

のように表示します。


「処理が成功した」だけでは通知しない

毎週正常に動くたびに通知すると、重要な警告が埋もれてしまいます。

そのため、原則として正常時は履歴だけを残し、次のような場合に通知します。

  • Collectorが失敗した
  • 一定時間、新しい観測値がない
  • 連続して複数回失敗した
  • 値が想定範囲から大きく外れた
  • データ形式が変わった
  • DB保存に失敗した
  • スケジュール自体が実行されなかった

通知先は、初期段階ではGitHub Actionsの失敗通知やGitHub Issueを利用し、将来的にメールやSlackなどを検討します。


異常値は自動削除しない

ドル円が前週から大きく変化した場合、データ取得ミスの可能性もあります。

しかし、本当に市場が急変している可能性もあります。

そのため、

大きく変動した
↓
自動的に削除

とはしません。

代わりに、

quality_status = warning

として保存候補に残し、人間が確認します。

確認内容は次のとおりです。

  • Federal Reserveの公式ページと一致するか
  • 日付や単位が入れ替わっていないか
  • 小数点の位置が正しいか
  • 別の通貨系列を取得していないか
  • 前後の観測値と整合するか

データが公式情報と一致していれば、急激な変化であっても正しい市場データとして採用します。


GPIを公開してよい状態か判断する

将来的にGPIを毎週自動計算するようになっても、Collectorに問題がある状態で通常値を公開してはいけません。

公開前には、各構成要素の状態を確認します。

USD/JPY:Fresh
DRAM:Fresh
NAND:Fresh
GPU需給:Fresh
海上運賃:Delayed

一部のデータが遅れている場合は、Rule Bookに従って、

  • 正式値を公開
  • Provisional(暫定値)として公開
  • 前回値をStale表示付きで使用
  • 公開を停止

のどれにするかを判断します。

この判断をCollectorの都合だけで変更せず、GPI全体の共通ルールとして管理します。


今回確定した運用方針

USD/JPY Collectorの初期運用では、次の方針を採用します。

  1. Federal Reserve H.10の更新頻度に合わせて取得する
  2. 観測日、公表日、取得日を分ける
  3. Collectorの実行履歴を保存する
  4. 成功・警告・失敗を区別する
  5. 通信障害だけを制限付きで再試行する
  6. データ形式変更などは人間の確認対象とする
  7. 同一値は重複保存しない
  8. 訂正値はRevisionとして残す
  9. データ鮮度を公開情報へ含める
  10. 自動実行と手動実行の両方を残す
  11. 正式なGPIを公開できない状態を検知する
  12. 正常時より、異常時の通知を優先する

まだ実装していないもの

今回のVol.4は、Collectorの継続運用設計が中心でした。

現時点ですべての機能が本番稼働しているわけではありません。

今後、段階的に追加するものは次のとおりです。

  • Collection Run専用テーブル
  • データ鮮度判定
  • 自動スケジュール
  • 制限付き再試行
  • 連続失敗の検知
  • GitHub Issueまたはメール通知
  • 管理用ステータスAPI
  • Collector監視画面
  • 本番用スケジューラー

一度にすべてを実装するのではなく、テストと運用結果を確認しながら追加する予定です。


次のVol.5で紹介すること

次回のGPI開発日誌 Vol.5では、今回設計した運用ルールを実装します。

主な内容は、

  • Collection Runテーブル
  • 最終成功日時の保存
  • Fresh/Stale判定
  • Collectorの自動実行
  • 再試行
  • 実行されなかった場合の検知
  • GitHub Actionsからの手動実行
  • 障害時の通知

です。

これが完成すればUSD/JPY Collectorは、

手動で動かせるプログラム

から、

毎週のデータ更新を任せられる運用システム

へ進化します。


まとめ

Vol.3では、Federal Reserve H.10からUSD/JPYを取得し、PostgreSQLへ保存する最初のデータパイプラインを作りました。

しかし、指数を継続公開するには、

一度動くことより、異常に気付きながら動き続けられること

が重要です。

今回のVol.4では、

  • 取得履歴
  • データ鮮度
  • 欠損検知
  • 再試行
  • 自動実行
  • エラー分類
  • 障害通知
  • GPI公開可否の判断

という継続運用の設計を行いました。

GadgetStreem Intelligenceは、単に市場データを集めるだけでなく、

データが正しく更新されているかまで説明できるプラットフォーム

を目指します。

次回は、この運用設計を実際のコードとデータベースへ落とし込みます。

GPIが継続的に更新される指数として完成するまで、引き続き開発過程を公開していきます。

ぜひ一緒に成長を見守っていただけるとうれしいです。


※GPIは現在開発中の実験的な市場分析指標です。
個別製品の将来価格、値上げ、値下げ、在庫状況、購入利益を保証するものではありません。
また、投資判断を目的とした金融指標ではありません。

この記事が気に入ったら
フォローしてね!

コメント

コメントする

目次