[ Data Infrastructure ]

バルク実行の設計 — earningsSchedule を起点にした並列処理

全社×全Qの決算データを順次取得すると数日かかる。rate limit と並列度のトレードオフを解決した earningsSchedule 起点のバルク設計と、実運用でわかった効果・落とし穴を整理する。

Author: 森本拓見
#claude-code #ai-driven-dev #batch-processing #data-engineering

はじめに

八雲では社内データ基盤として Medallion を開発している。東証プライム・スタンダード・グロース全3市場、過去5年分、四半期ごとの決算データを収集・構造化する基盤だ。対象銘柄は最終的に約3,800社を想定している。

試作段階ではシンプルに for ループで回した。1社あたり平均20〜30秒。3,800社を直列で処理すると単純計算で20〜30時間かかる。この記事では、earningsSchedule シートを起点に対象を絞り込み、schedule_to_tickers.pybulk_runner.sh で並列実行するバルク設計の詳細と、運用してわかったことを書く。


課題: rate limit と並列度のトレードオフ

単純な並列化はすぐに別の壁にあたる。

決算データの取得経路は主に3つある。JPX の東証上場会社情報サービス、TDnet の速報、各社の IR ページだ。JPX と TDnet は明示的な rate limit ポリシーを持ち、高並列でリクエストを投げると短時間でブロックされる。

GWS Sheets API も制限がある。書き込みは1分あたり100回程度で、30並列で取得結果を次々書き込むと 429 Too Many Requests が返ってくる。かといって直列では遅すぎる。

もう一つの問題は「全3,800社を毎回処理しなくていい」という点だ。決算発表は銘柄によって日程が違う。無差別にバルク実行すると、発表前や処理済みの銘柄も巻き込んで無駄な処理が増える。


アプローチ: schedule_to_tickers で対象を絞り、bulk_runner で並列実行

解決策の中核は2段階のパイプラインだ。

第1段階 — schedule_to_tickers.py による絞り込み

earningsSchedule シートを走査し、実行対象の ticker を JSON で出力する。--schedule-month 2026-04 を渡せば「2026年4月に決算発表予定の銘柄」だけを抽出できる。

# 4月発表予定の銘柄を抽出
python3 schedule_to_tickers.py --schedule-month 2026-04

# 直近30日に発表があった銘柄
python3 schedule_to_tickers.py --schedule-recent 30

シートは Ticker / Company / FY_End / Announcement_Date / Market / Fiscal_Month 列を持つ複数タブ構成(年度別)で、全タブを自動検出して走査する。重複銘柄はユニーク化し、equity-ir-urls.json で社名・URL テンプレートを補完する。

第2段階 — bulk_runner.sh による並列実行

# 4月発表予定を4並列で取得
bash bulk_runner.sh --schedule-month 2026-04

# 対象銘柄確認のみ(ドライラン)
bash bulk_runner.sh --schedule-month 2026-04 --dry-run

処理フローは3ステップだ。まず schedule_to_tickers.py を呼んで対象 ticker の JSON を取得し、次にその ticker 配列をバッチ並列で処理し、最後に成功・失敗件数を集計する。


並列度(4並列等)と失敗時のリトライ

bulk_runner.sh の並列制御は GNU parallel に依存せず、bash のサブシェル起動と wait を組み合わせた簡易実装だ。PARALLEL 個のサブシェルが溜まったら先頭を wait するスライディングウィンドウ方式で、macOS の bash 3.2 でも動くよう mapfile を避けて実装している。

リトライは history_runner.sh 内で実装している。HTTP 429 や接続タイムアウトに対して指数バックオフ付きのリトライを3回まで行い、それでも失敗した場合に fail:ticker を返す。バルク実行後に fail_tickers が残っていれば、翌日のバッチで再処理される。


進捗ログの保存と再開可能性

ログは ~/Library/Logs/montage-earnings-bulk.log に追記する。タイムスタンプ付きで全イベントを記録するため、失敗時の調査がしやすい。

再開可能性は launchd のスケジュールで担保している。毎朝5時に bulk_runner.sh --schedule-recent 30 が自動起動し、直近30日の発表銘柄を毎日再試行する。一時的なネットワーク障害や JPX の定期メンテナンスで失敗した銘柄も、翌日には自動回収される設計だ(launchd-tdnet の設計 参照)。

冪等性も重要な設計方針だ。history_runner.sh は GWS Sheets の既存行を確認してから更新するため、同一 ticker を複数回実行しても結果は変わらない。


運用してわかった効果と落とし穴

効果

4並列化だけで直列比で約3.5〜4倍に短縮された。287社を実行した際、直列推定2時間40分のところを約1時間15分で完了した。--dry-run で事前確認できるため、誤った月指定で無駄なリクエストを投げるミスもなくなった。

落とし穴1: bash の配列エクスポート制限

export -f で関数をサブシェルに渡しても、配列変数 EXTRA_HISTORY_ARGS はエクスポートできない(bash の仕様)。最終的にサブシェルブロック ( ... ) & で配列をそのまま継承させる方式に変更した。親シェルの配列はサブシェルに自動継承されるため、export 不要で動く。

落とし穴2: GWS Sheets の429

1銘柄あたり pl/bs/cf/per_share/forecast 等8タブを更新するため、4並列では最大32の同時 API コールが発生することがある。現在は history_runner.sh 内で Sheets 書き込みを500ミリ秒間隔で直列化しており、429はほぼ発生しなくなった。

落とし穴3: Announcement_Date 未入力行

発表日未確定の銘柄は Announcement_Date が空白のままになる。フィルタ関数は None のレコードをスキップするため、これらはバルク実行で拾われない。--tickers-file で直接指定するか、毎月初に手動で earningsSchedule を更新する運用としている。


まとめ

  • schedule_to_tickers.py で earningsSchedule シートから対象 ticker を月・直近N日で絞り込む
  • bulk_runner.sh がその JSON を受け取り、4並列でバルク処理する
  • launchd で毎朝5時に自動実行し、直近30日の銘柄を継続的に再試行する
  • 失敗銘柄はログに記録され、翌日のバッチで自動回収される

Medallion の XBRL パーサ(xbrl-parser の設計)や、GWS Sheets との連携ハブ(medallion hub の設計)、launchd による TDnet 速報追従(launchd-tdnet の設計)とあわせて読むと全体像がつかみやすい。

ShareShare on X