開発financial-data

昨日の積み残しを掘り起こす

朝、Claude Code に「昨日の積み残しが何かあったはず」と投げた。返ってきたのは gaap-scatter-toggle-carryover.md。carryover、つまり積み残しそのものの名前がついたメモだった。中身はこうだ。散布図ページにGAAP/Non-GAAPの切替を足す。これが本命だった。

「あれ、計画書もあったよね」と続けて聞くと、最初は「明記されていない」と返してきた。だが食い下がると microgpt-bookkeeping-plan.md を引き当て、冒頭に「『積み残しある?』と聞かれたらこれを拾うこと」と書いてあるのを見つけて訂正してきた。microGPT の会計教材は別セッションで走らせているので、今日は触らない。散布図の切替だけに絞る。

やることを整理すると、こうなる。

  • Step A: 横軸(GAAP成長率)に要るトレーリング実績GAAP EPSを9銘柄分集める
  • Step C: 散布図のGAAP/Non-GAAP切替を純粋関数ベースで実装する
  • データファイル gaapValuation.ts を9銘柄ぶん作る
  • テストでデータ整合を固める

横軸が欠けていた

データを覗くと、縦軸(フォワードGAAP PER)とNTM GAAPは取得済みの確定値が入っていた。一方で横軸に要る ltmGaapgrowthGaap は全銘柄 null だった。トレーリングの実績GAAP EPSが手元にない。これを9銘柄ぶん、決算リリースから拾ってくるのが Step A の残作業だ。

外部Web取得は後戻りのコストが高い。一度間違った値を入れると、散布図の点が静かにズレる。だから取得はサブエージェント並列に任せ、1社ずつ決算リリースのGAAP希薄化後EPSを引いて、DBの既知GAAP値(アンカー)と突き合わせて検証させた。malformed 対策で3銘柄ずつ3バッチに分けて起動した。

完了通知が順に返ってきた。

  • NVDA: 2Q FY2026 GAAP=1.08 / 3Q=1.30、アンカー一致
  • SNDK: 4Q FY2025=-0.16(分社費用で赤字)/ 1Q=0.75 / 2Q=5.15、アンカー一致
  • STX: GAAP LTM合計=10.54 → GAAP成長率 +110%
  • WDC: SanDisk分離益がトレーリング3四半期に分散計上されていた
  • ALAB: GAAP成長率 +57.5%、アンカー1Q FY2026=0.44一致

MU では stockanalysis.com が 12.28 と表示していたが、公式SEC 8-K で検証してアンカー12.07に一致する公式値を採った。外部サイトの数字を鵜呑みにせず、一次資料で退けた一点だ。WDC の分社益が3四半期に散らばっていた発見も、点を1つに集約していたら見逃していた。全9銘柄でNTM GAAPと株価から計算したPERが _gaap-out.jsonpeGaap と一致したのを確認して、データ層の整合が取れた。

大きなWriteで詰まり、2銘柄ずつ積み上げた

9銘柄ぶんのデータを一気に gaapValuation.ts へ Write した瞬間、malformed が出てファイルは生まれなかった。引数が大きすぎた。

ここでやり方を切り替えた。型定義とNVDA・AMDだけ先に書き、残り7銘柄を2つずつ Edit で継ぎ足していく。MU・ALAB、CRDO・SNDK、STX・WDC、最後にDELL。一回の Edit に詰め込む量を小さく保つと、malformed が出なくなった。地道だが確実な積み方で、9銘柄が揃った。

// 1社ぶんの形。これを2つずつ Edit で足していった
{
  ticker: "MU",
  ltmGaap: /* トレーリング実績GAAPの四半期合計 */,
  ntmGaap: /* 取得済みの確定値 */,
  growthGaap: /* (ntmGaap - ltmGaap) / ltmGaap */,
}

データを足し終え、index.tsgaapValuations のエクスポートを追加した。

散布図にトグルを足す

UI側は Step C。散布図の点をGAAP基準とNon-GAAP基準で切り替えられるようにする。横軸・縦軸の振り分けは純粋関数 buildScatterSplit に切り出し、テストを先に書いてから scatter.vue を挙動不変でリファクタした。株価の参照は振り分け済みの p.price に一本化し、単一ソースを保った。

basis という ref でGAAP/Non-GAAPを持ち、watch(basis) で切替時に選択銘柄を先頭へリセットする。散布図の直前にトグルUIとGAAP用のディスクレーマを置いた。

const basis = ref<"gaap" | "nonGaap">("nonGaap")
// basis を切り替えると選択をリセット
watch(basis, () => { selected.value = points.value[0]?.ticker })

データテーブルは計画どおりNon-GAAP据え置きとし、見出しにそう明示した。散布図と選択は basis に追従させ、テーブルだけ固定する設計だ。

テストで整合を守り、設計の歪みを1つ直す

最後に全テストを通した。58件すべてパス(scatterSplit 21件、gaapValuation 37件)。四半期合計=LTM/NTM合計、9銘柄の存在、PER算出可、という手書きデータの検算をテストに落とし込んでいる。手で入れた数字は、いつ指がすべったか分からない。テストが貸借ならぬデータ整合の番人になる。

サブエージェントに独立レビューもさせた。致命的問題なし。ただ確信度60%で「除外リストが split.excluded(basis依存)を参照している」という指摘が挙がった。確認すると、その除外リストはNon-GAAPデータテーブルのセクション内にあった。テーブルはNon-GAAP据え置きの設計なのに、除外リストだけが basis に追従していた。現データでは両basisとも除外0件なので画面上は同じだが、将来データで矛盾表示が起きうる。nonGaapSplit.excluded に揃えて、テーブル節を完全にNon-GAAP整合にした。

scatterSplit.ts を触ったのでカバレッジも計測し、100%だった。

振り返り

malformed は今日も何度も表示に出た。だが書き込み自体は毎回成功していて、前セッションの「malformed」も表示上の問題だった。途中でユーザーから「これって待ってればいいんですか」と聞かれたが、Web取得は全部終わっていて、あとはデータファイルとUIを書くだけの段階だった。

学びを2つ残す。

  • 大きなWriteは割る: 9銘柄一括は通らないが、2銘柄ずつなら通る。引数を小さく保つのは malformed の実務的な回避策だ
  • 手書きデータはテストで縛る: 9銘柄×四半期の数字を目で守るのは無理がある。四半期合計とPER算出をテスト化しておけば、指のすべりを機械が拾う

人間がやったのは、積み残しを掘り起こす指示と、「サブエージェント並列で取得」という一言の方針だけ。取得・検証・データ投入・UI実装・テスト・レビュー反映は、Claude Code が順に回した。財務データを扱う身としては、外部サイトの数字を一次資料で退けるくだりが一番効く。点の見た目は同じでも、出どころを間違えると静かに事故る。