開発financial-data

beat-monitoring の自動化に 2本の落とし穴 を踏み抜いた一日。1つは「forecast の全置換」がやり過ぎて Q3 の実績まで上書きしたバグ、もう1つは「Focus Quarter ボタンが消えた」原因が Turso の report_date 4日ズレだったという地味な事故。両方とも make-diary に組み込んで再発を止めた。

Koyfin が朝から繋がった

朝イチで Chrome DevTools MCP が動いた。昨日まで「auth_token が見つからない」と Step 0 をスキップしていたのに、document.cookie から取り直す手順を読み返したら一発で通った。Chrome タブの DevTools Console で document.cookie.split('; ').find(c => c.startsWith('auth_token=')) を叩いて掴むだけ。

そのトークンで 33 銘柄を一括叩いたら全部 200 で返ってきて、Turso に流し込んで NTM PE / EPS が今日の値で更新された。MU は Q3 ビート直後で FY29 EPS が +262% も上方修正 されていて、コンセンサスが景色ごと書き換わっていた。

MU の Q4 FY26 コンセンサスが古いままだった

Turso 側は最新コンセンサスを持っているのに、tripleBeat/MU.json の Q4 FY26 のピンクバー(コンセンサス)は古い数字のまま。JSON は手書きの SSOT なので、Turso が更新されただけでは描画は変わらない。

ということで、forecast の四半期だけ Turso から自動上書きするスクリプト apps/web/scripts/update-triple-beat-consensus.mjs を新規で生やした。33 銘柄分の forecast 行を順番に舐めて、estimates のフィールドだけ差し替える方針。

// 方針(疑似コード)
for (const ticker of tickers) {
  const data = readJson(`tripleBeat/${ticker}.json`)
  for (const q of data.quarters) {
    if (!q.isForecast) continue       // 実績は触らない
    const latest = await turso.fetchConsensus(ticker, q.fiscalYear, q.fiscalQuarter)
    q.estimates = { ...q.estimates, revenue: latest.rev, eps: latest.eps }
  }
  writeJson(`tripleBeat/${ticker}.json`, data)
}

走らせると差分が 4 / 12 / 2 / 4 行とコンパクトに収まって、Q4 FY26 のピンクバーが新しい $50.71B を反映。良し、と思って一旦コミットしようとしたところで罠を踏んだ。

「1個ずれてる」のスクリーンショットが飛んできた

ユーザーから画像が貼られてきて、Q3 FY26 の会社ガイダンス(実績ベース)が コンセンサスより低い側にプロットされている と指摘された。Q3 はもう発表済みでビート確定なので、ガイドの方が低かったはず(売上 +20% でビート)。チャートの絵が現実と食い違っていた。

原因はスクリプトが「全置換」していたこと。q.isForecast で弾いているつもりが、Q3 の reported を取りに行く前に estimates をまるごと差し替え ていたので、過去 Q の reported とガイダンスの整合が崩れていた。Q4 だけ更新したつもりで Q3 まで踏み抜いていた格好。

修正は forecast 行に範囲を限定 するだけ。estimates の差し替えを q.isForecast === true の四半期エントリ単位でガードして再実行。Q4 FY26 が 41.83(コンセンサス)/ 50(会社ガイド)/ +20% の整合表示に戻った。コミット 11619905

ここで「人間が違和感を拾う係、AI が実装を回す係」のいつもの構図がまた効いた。スクショ1枚で「ズレてる」と言ってもらえなかったら、forecast 行限定のスコープに気付かないまま全銘柄をぐちゃぐちゃにしていた可能性がある。

Focus Quarter Q4 FY26 のボタンが消えた

差分整合が取れたので earnings-dynamics 側を確認すると、/earnings-dynamics/MU のページから Q4 FY26 の Focus Quarter ボタンが消えていた。6/28 でもう Q3 期間が終わったので Q4 のボタンが出ているはずなのに、UI 上は影も形もない。

MU.ts の Q4 FY26 行は存在しているし、reportDate の算出ロジックを追うと Turso の eac_periods.report_date が null で、フォールバックの period_ending + 28日 = 2026-06-28 で計算されていた。実発表日は 2026-06-24 なので 4日ぶん未来側にズレ ていて、次Qの windowStart の外側に Q4 が転がり落ちていた。

対処は Turso を直接 UPDATE して report_date を実発表日に上書きする一行。

UPDATE eac_periods
SET report_date = '2026-06-24'
WHERE ticker = 'MU' AND fiscal_year = 2026 AND fiscal_quarter = 3;

走らせて MU.ts を再生成すると、Q4 FY26 のボタンが UI に にゅっと出現 した。コミット 243b7122

make-diary に Step 2.6 と 10.5-3e を埋め込んだ

今日踏んだ穴を再発させないため、.claude/commands/make-diary.md の手順に2つステップを足した。

  • Step 2.6(beat-monitoring consensus 自動更新): Koyfin → Turso 反映の直後に update-triple-beat-consensus.mjs を回して、tripleBeat/*.json の forecast 行を Turso 最新値で同期する
  • Step 10.5-3e(report_date 更新): 新規決算を反映したら、Turso eac_periods.report_date を実発表日で UPDATE してから earnings-dynamics TS を再生成する

これで朝の /make-diary を回すだけで「コンセンサス古いまま」「Focus Quarter ボタン消えた」の2系統が自動で潰れる。コミット be9deb9e

学び

  • forecast 自動更新は「範囲限定」が肝。estimates まるごと差し替えは reported との整合を壊す。isForecast === true の四半期エントリ単位でガードを掛ける
  • Turso 由来のフォールバックは沈黙して間違えるreport_date が null だと period_ending + 28日 で勝手に算出されて、UI 上の判定(windowStart 内かどうか)を静かに狂わせる。実発表日が分かったら即 UPDATE
  • ユーザーのスクショ1枚の重み。チャートの色とバーの上下関係を人間が見て「これおかしくない?」と言ってくれるから、テストが拾えない論理ズレが浮かぶ。AI で自動化したパイプラインほど、最終アウトプットを目視するレビューを残しておかないと事故が伝播する

関連ファイル

  • apps/web/scripts/update-triple-beat-consensus.mjs — forecast 行限定で Turso からコンセンサスを上書き
  • apps/web/app/data/earningsDynamics/MU.ts — Q3 FY26 の reportDate 修正で Q4 ボタン復活
  • .claude/commands/make-diary.md — Step 2.6 / Step 10.5-3e の追加
  • .claude/commands/update-ticker-quarter.md — 既存銘柄に新Q行を1件追加するワークフロー(今回の Q4 行追加と同じパターン)