開発financial-data

外部分析チームの Google スプレッドシートを開きながら「ハイパースケーラーの CapEx をまとめたページがあったはず。あれに営業CFも並べたい」と頼んだのが朝の最初の依頼だった。たった1セクション足すだけの軽い作業のつもりだったのに、終わってみれば Koyfin の TSV 取り込みパイプラインの隠れバグを2本踏み抜いて、半日が溶けていた。

ページの構成を思い出す

/memory-makers/hyperscaler-capex の中身を Claude Code に追わせて、データの出所を整理させた。

  • 年次 CapEx は外部分析チームのスプレッドシート由来(hyperscalerCapexAnnual
  • 四半期 CapEx は Koyfin EAC 由来(hyperscalerCapexQuarterly

CapEx と同じ Koyfin EAC テーブル(eac_quarterly)にメトリクス Cash-from-Operations がそのまま入っているのが分かったので、generate-hyperscaler-capex-quarterly.mjs をコピーしてメトリクス名だけ差し替えた generate-hyperscaler-ocf-quarterly.mjs を生成させた。

ページ側は CapEx の「四半期テーブル」直後・「年次 CapEx」の前に、同じ構造(個社別小チャート → 全社合計 → 比較テーブル)でセクションを差し込んだ。ついでに「CapEx / 営業CF 比率」テーブルも追加して、設備投資が営業 CF の何 % を食っているかを並べた。

ここまでは30分で済んだ。崩れたのはこの後の「実データ更新」だった。

DB の最新実績が半年止まっていた

スクショで dev サーバー上のページを見ると、最新実績クォーターが CY2025Q3 までしか出ていない。MSFT は5月に CY2026Q1(= 3Q FY2026A)の決算を出しているはずなので、画面の数字がちょうど半年遅れている。

/check-earnings の日次更新対象は NVDA / MU / SNDK の3銘柄だけで、ハイパースケーラー7社は 手動で Koyfin EAC を引いて取り込む運用だった、と Claude Code が思い出させてくれた。

Chrome 拡張の postMessage ブリッジで7社一気にDL

chrome-extension-kofyin には Phase 2 で実装した postMessage ブリッジ(window.__kofyin.runSingleDownload(...))が既にある。Chrome DevTools MCP 経由で MSFT タブを操り、

  1. ticker ごとに EAC ページの URL を解決
  2. runSingleDownload を呼んで TSV をダウンロード

を Claude Code に順次回させた。1社あたり約30秒。MSFT → GOOGL → AMZN → META → AAPL → ORCL → CRWV の7社で3分強。並列にしたい欲を抑えて直列で回したのは、Koyfin 側の rate limit を踏みたくなかったから。

DL したファイルが「ダウンロード」「ダウンロード (1)」…「ダウンロード (8)」という汎用名で保存されていて、subfolder/filename 指定が効いていない様子だった。中身の TSV は正しく ticker と timestamp を持っていたので、リネーマ経由で actual-and-consensus/2026-06-26/ に正規ファイル名で並べ直してから SQLite に流し込んだ。

「TSV さえ正しければ取り込みは通るはず」とこの時は思っていた。

取り込みが通ったのに数字がズレる

eac_tsv_to_sqlite.py が「7社インポート成功」と出して、apps/web/data/koyfin.db に同期したあと、念のため MSFT の最新行を直接 SELECT した。出てきた結果が、

1Q FY2026A = Mar-31-2026

Koyfin の画像と照らすと、画像側では同じ 1Q FY2026ASep-30-2025。6ヶ月ズレている。画面の数字がうまそうに見えても、period_ending の月日が違うので意味が反対になっている。**取り込みが「通った」のに「正しくない」**というのが一番たちが悪い。

バグ1: CY(Calendar Year)ラベルを弾いていた

eac_tsv_to_sqlite.py を grep で読んでもらうと、200行目あたりで

if header_row[0] not in ("Fiscal Years", "Fiscal Quarters"):
    return  # スキップ

という早期 return が刺さっていた。今回 Koyfin から落ちてきた TSV のヘッダは Calendar Years / Calendar Quarters。条件にヒットせず 全行スキップ されていた。「7社インポート成功」のログは出るのに DB は1行も更新されない、という嫌な状態。

修正は ("Fiscal Years", "Fiscal Quarters", "Calendar Years", "Calendar Quarters") の4本立てに広げるだけ。fetched_at2025-12-14 のまま固まっていた既存6社のレコードを一旦削除してから、再インポートさせた。

バグ2: period_ending が「全社共有」の設計だった

これでも MSFT の period_ending は妙な日付のまま。さらに掘ると get_or_create_eac_period(period_label, ...)ticker を引数に取らず、period_label だけで periods 行を共有していた

具体的には、ORCL は5月決算なので 1Q FY2026A の period_ending は Aug-31-2025。それが先に DB に入っていたために、後から取り込んだ MSFT / GOOGL / AMZN の 1Q FY2026A も同じ Aug-31-2025 を参照してしまう。Koyfin の 1Q FYxxxxA というラベルは会計年度を全社で共有してはいけない概念なのに、テーブル設計が共有を許してしまっていた。

「設計を直す」のが正攻法だが、ratio table が壊れたまま明日に持ち越したくなかったので、最短のワークアラウンドとして CY ラベル(1Q CY2026A 等)の period_ending を カレンダー末日に一括 UPDATE した。CY ラベルは定義上 calendar quarter なので、ticker 横断で同じ日付になっても問題が起きない。

UPDATE eac_periods
   SET period_ending = '2026-03-31'
 WHERE period_label = '1Q CY2026A';

これで MSFT の最新実績が 1Q CY2026A = Mar-31-2026 = 46,679 に揃って、Koyfin の画像と数字も日付も完全一致した。

dev サーバーの HMR が取りこぼした

生成スクリプトを再実行して TS データを吐かせたあと、ページをリロードしても「最新実績: 2025Q3」のまま動かない。データ TS を cat で直接確認すると 2026Q2 まで入っている。dev サーバーが app/data/memory-makers/ 配下のファイル変更を拾い切れていない様子だった。

Windows + pnpm dev でたまにある現象なので、3000番のプロセスだけを Get-NetTCPConnection -LocalPort 3000 で特定して落とし、pnpm dev を立て直した。リロード後、「最新実績: 2026Q2」表示で MSFT の46,679も乗った。OCFセクションも個社別チャート・全社合計・比率テーブルが全部描画された。

CapEx / 営業CF 比率テーブルは2025Q1〜Q3 の3行表示。AAPL と CRWV の Freeプラン制限で2025Q4以降が全社揃わないので絞り込みが効いて3行に減っているのが原因で、ロジックは正しい。

おまけ: 簿記3級ページの HTML 仕様違反

作業中、dev サーバーのコンソールに

<table> 直下に <tr> があります(<tbody> で包んでください)

という警告が accounting-app-play-gimmicks.vue から流れていた。ハイパースケーラーの作業とは無関係だが、目に入った警告は今のうちに潰しておきたいタチなので、該当箇所2つを <tbody> で包んで終わらせた。「ハイパースケーラーの確認のためにそのページ開いてくれてたんですよね?」と聞かれて慌てて軌道修正したが、警告ゼロにはなった。

学び

  • 「取り込みが通った」ログを信用しない。画面の数字と外部の正本(今回は Koyfin の画像)を1組だけでも目視で突き合わせると、period_ending のズレのような「成功扱いのバグ」を1分で踏める
  • eac_periods のような「外部キーで共有される寸法表」は、共有していい単位(CY ラベル)と共有してはいけない単位(FY ラベル)を、テーブル設計の段階で分けるのが正しい。今回は CY ラベルの一括 UPDATE で逃げたが、FY ラベル側を ticker ごとに分離する改修が宿題として残っている
  • Chrome 拡張の postMessage ブリッジは「Chrome DevTools MCP からポップアップをクリックできない」問題の優れた回避策。7社×30秒で3分強で済むので、月次運用ならこれで十分

明日に残したこと

進捗は memo/2026-06-26/hyperscaler-capex-ocf-progress.md に置いた。明日やりたいのは、

  • get_or_create_eac_period を ticker 単位に分離する設計改修(FYラベル側)
  • AAPL / CRWV の Freeプラン制限で歯抜けになる列の扱い方(直近4Qのみ表示にする等)
  • CapEx / 営業CF 比率テーブルの空欄行を「N/A」で埋めて行数を保つ

関連ファイル

  • ページ: apps/web/app/pages/memory-makers/hyperscaler-capex.vue
  • CapEx 四半期スクリプト(元ネタ): apps/web/scripts/generate-hyperscaler-capex-quarterly.mjs
  • 今回作った営業CF スクリプト: apps/web/scripts/generate-hyperscaler-ocf-quarterly.mjs
  • 取り込みバグ本体: chrome-extension-kofyin/scripts/eac_tsv_to_sqlite.py
  • 進捗メモ: memo/2026-06-26/hyperscaler-capex-ocf-progress.md