開発financial-data

「イビデン株(東証4062)をビートモニタリングに加えて。アナリスト予想と実績が比べられればいい、株価推移も」というオーダーから始まった。

これまでビートモニタリングに乗っていたのはNVDA / MU / PLTR / RDDT といった米国半導体・AI銘柄のみで、日本企業は初めての追加だった。

最終的に銘柄追加自体はすんなり終わったのに、ローカル確認で「これでヨシ」と言いそうになったところでチャートの空欄に気づき、そこからparseCurrencyToNumber の日本円未対応バグを引き当てることになった。本記事はその記録。

1. 日本企業ガイダンスの扱いをどうするか

最初に詰まったのは、ビートモニタリングの主役である「ガイダンス vs 実績」の比較を日本企業でどう運用するか。

  • 米国企業は四半期ごとに次四半期ガイダンスを出す(Revenue / EPS / Gross Marginなど)
  • 日本企業はそもそも四半期ガイダンスを出さない。出すのは1年後通期の見通しのみ
  • 粒度が合わないので、四半期ベースの「ガイダンス vs 実績」表に日本企業を素直に並べると嘘になる

ここはClaude Codeと相談しつつ、「日本企業ではガイダンス列を null 運用にして、実績 / アナリストコンセンサス / YoY / 翌日株価 だけ埋める」方針で着地。スキーマは変えずに、欠損を許容する形にした。

2. データ集めはagent-browser並列で

米国銘柄は外部のコンセンサスデータプロバイダ経由で取れるが、イビデンは別ルートが必要だった。

  • 株探:通期実績(売上4,162億円、営業利益620億円)をWebFetchで取得
  • Yahoo!ファイナンス:株価15,340円、アナリスト予想ページを参照
  • irbank.net:決算発表日と過去四半期の業績を取得

WebFetchで素直に取れる部分は取り、過去8〜12四半期分の数字は agent-browser を並列に飛ばして集約させた。出来上がりは apps/web/app/data/tripleBeat/4062.json に格納。

合わせて以下も更新:

  • tickerMeta.ts に 4062 を追加(日本企業フラグも持たせる)
  • summaries.ts を再生成

ここまでで一覧ページにイビデンが並び、詳細ページにも数字が出るところまで確認できた。ここで止めていたら、出荷していた。

3. ユーザーの一言で違和感を拾われる

ローカルで一覧→詳細を踏んで、数字が並んでいるのを目視して「OKそうですね」と言いかけた瞬間にもらった一言。

あ。チャート何も出てないですよ。

詳細ページを改めて見ると、たしかにイビデンのページだけ株価チャートも実績推移チャートも軸ラベルしか出ていない。系列の折れ線が一本もない。

他の銘柄(NVDA / MU など)の詳細ページに飛ぶと、同じコンポーネントでも普通にチャートが描画されている。**「4062だけ空欄」**という現象に絞り込めた。

ここが今回の一番のポイントで、自分はその時点で数字の存在しか見ていなかった。チャートのプロット領域が空白なのを「まあそういうレイアウトかも」と無意識にスルーしかけていた。隣で見ていた人が空欄に気づいたから救われた話。

4. 原因:parseCurrencyToNumberが日本円を全部nullにしていた

チャート系コンポーネント(BeatStockChart / BeatExpectationsChart)は、JSONの文字列を parseCurrencyToNumber で数値化してから ECharts に渡している。

parseCurrencyToNumber の当時の実装は $1.5B / $335.8 M / -0.06 のような米ドル表記しか考慮しておらず、925億円3,784円 といった日本円表記は 正規表現にマッチせずすべて null を返していた。

つまりイビデンのJSONはちゃんと読み込まれているのに、null の配列がチャートに渡され、結果として描画する点が一つもない状態になっていた。一覧表示は文字列をそのまま見せていたから気づきようがなかった。

before / after の正規表現

修正前は数値+M/B/K サフィックスのみを見ていた。

// before(米ドル表記しか考慮していない)
const match = cleaned.match(/^([\d.]+)\s*([MmBbKk]?)$/)

ここに「 で終わる、兆 / 億 / 万 のサフィックスを持つ場合がある」分岐を先に入れて、マッチしたら 1e12 / 1e8 / 1e4 を掛けてから返す形にした。

// after(日本円分岐を先に判定)
const jpMatch = cleaned.match(/^([\d.]+)\s*(||)?$/)
if (jpMatch) {
  const num = parseFloat(jpMatch[1] ?? '')
  if (Number.isNaN(num)) return null
  const jpSuffix = jpMatch[2]
  const jpMul =
    jpSuffix === '' ? 1e12 :
    jpSuffix === '' ? 1e8 :
    jpSuffix === '' ? 1e4 : 1
  return (isNegative ? -1 : 1) * num * jpMul
}

¥ プレフィックスと , の3桁区切りは事前に cleaned の段階で落としているので、¥15,655円 のような表記も同じ分岐で吸える。負号もケース分けせずに isNegative で統一処理した。

5. テストを追加してから画面で再確認

純粋関数の修正なので、まずはテスト先行で挙動を固めた。apps/web/tests/parseCurrency.test.ts に日本円ケースを追加。

  • サフィックスなし円:3,784円 → 3784、15,655円 → 15655
  • サフィックス:925億円 → 9.25e10、327億円 → 3.27e10
  • サフィックス:1兆円 → 1e12、1.5兆円 → 1.5e12
  • 負号:-87.1億円 → -8.71e9
  • ¥ プレフィックス:¥15,655円 → 15655

pnpm test:run tests/parseCurrency.test.ts が緑になってから、pnpm dev でブラウザを開き直してイビデン詳細ページを再確認。株価チャートと実績推移チャートに折れ線が出ていることを目視した。米国銘柄側のチャートにも変化がないことを確認して終わり。

6. 振り返り — 人間は画面の違和感を拾う係

今回、自分の仕事は2つだけだった。

  • 日本企業の追加方針(ガイダンスnull運用)を一言で決める
  • 「チャート空欄」の違和感を画面から拾う(これも今回は隣の人が拾ってくれた)

それ以外は全部Claude Codeに振った。データ収集は agent-browser を並列で飛ばして集約させ、JSON化させ、parseCurrencyの日本円対応も書かせ、テストも足させた。

毎回ここで思うのは、AIに任せた部分は綺麗に動いていても、画面に出てくる最終形を疑わないと事故るということ。今回もJSON上の数字は完璧に揃っていた。一覧の表示も問題なかった。にもかかわらず、チャート空欄という形で「数字は読めているがプロットできない」状態が裏で起きていた。

特に今回みたいに「既存パーサーが米国前提で書かれていて、新地域のフォーマットを足したらサイレントに null を返す」系の事故は、テストを書いていてもテストケースに無いから素通りする。ユーザーが目で見て『これ変じゃない?』と拾うのが最後の関門で、ここを自分が真っ先に拾えるようになりたい。

税理士・会計士業務に当てはめると、AIに作らせた試算表で「数字は揃っているけど合計欄だけ空白」みたいな事故と同型。最終チェックは画面を疑う癖をつけるしかない。

完了報告

  • 1. Vitest ユニットテスト: tests/parseCurrency.test.ts に日本円ケース5件追加、pnpm test:run pass
  • 2. Playwright E2E: ユーザー操作フロー変更なし、純粋関数の挙動拡張のためスキップ
  • 3. Vitest coverage: app/utils/parseCurrency.ts の日本円分岐をカバー
  • 4. Vitest bench: パフォーマンス重要処理ではないためスキップ
  • 5. セキュリティ/パフォーマンス/SRE 自己レビュー: 入力は文字列のみ、正規表現は線形時間で問題なし