• #ポートフォリオ
  • #スプレッドシート
  • #株価
  • #設計
  • #Google Workspace
開発financial-data

父から「楽天証券の画面、見るたびに数字を打ち直してて面倒だ」と言われて、保有銘柄を毎朝勝手に更新するスプレッドシートを作ることにした。今日はその設計を詰めた一日。まだ1行も本実装していない。設計だけで朝が溶けた。

きっかけは、父の楽天証券の保有銘柄一覧のスクショを1枚受け取ったこと。米国株が数銘柄、投資信託、それと外貨預り金が並んでいる。これを手で管理し続けるのは無理がある。保有銘柄・保有数量・平均取得価格・現在値・現在のドル・時価評価額、この項目をデータベース化して、Googleスプレッドシートに転記して、株価は毎朝更新したい。最終的には /update-portfolio という1つのスラッシュコマンドに畳み込む。方針はそこまで決めて、あとは設計をClaudeに書かせた。

まず「本当に値が取れるのか」を潰す

設計で一番怖いのは「綺麗な設計を書いたあとで、肝心の株価が取れませんでした」というオチだ。なので最初に、楽天の表示値と一致する株価ソースがあるかを実証してから設計に入らせた。

米国株と為替USD/JPYは、Stooqの無料CSVで取れた。APIキー不要、curlでHTTP 200が返る。しかも取得した終値が父の画面の現在値とぴったり重なる。為替も楽天の表示と一致した。ここで「データは正確だ」と確信が持てた。

問題は父が持っている投資信託だった。これは米国上場のETFではなく日本の投資信託で、基準価額は円建て・1万口あたりで動く。株価CSVには載らない。協会コードを調べてから、みんかぶ投信のページをcurlで叩いたら、HTMLの中に基準価額と前日比が埋まっていて、これも楽天の表示と完全一致した。Yahoo!ファイナンスも予備として値が合うことを確認した。

ここまでで「全データがブラウザを介さず、curlとNode単体で揃う」と分かった。これが大きい。ブラウザ自動化を挟むと毎朝の無人実行が一気に脆くなる。curlだけで閉じるなら、Windowsのタスクスケジューラに登録して放っておける。

自動実行の手段を1つ捨てた

最初はClaude内蔵のcron(CronCreate)で毎朝動かそうかと考えた。だが調べさせたら、セッション依存・7日で失効・REPLがアイドルの時だけ発火、という性質が出てきた。毎朝確実に動かしたい用途には向かない。ここはWindowsタスクスケジューラ+スタンドアロンのNodeスクリプトに切り替えた。Claudeを起動していなくても、ローカルに保存済みのgws認証だけで回る形にする。

Codexと3往復して、設計の前提が1つ崩れた

設計書を書かせて、提示前にCodexレビューにかけた。ここで致命的な指摘が3点返ってきて、そのうち1点で前提が崩れた。

指摘は「楽天の評価損益率を、外貨ベースの値上がり率で再現しようとしている。だが楽天の表示は円建て(取得時の為替込み)のはずだ」というもの。半信半疑だったので検算させた。ある米国株について、外貨ベースの値上がり率を平均取得価格から計算すると、楽天の画面に出ている損益率と数字が合わない。差分は、購入後に進んだ円安ぶんだった。取得時の為替はおよそ154円、今は159円台。この差が為替差益として損益に乗っている。

つまり「平均取得価格(USD) × 現在の為替」で取得原価を出すと、為替差益が丸ごと消えて楽天の表示とずれる。ここで設計を変えた。米国株の取得原価は円建ての静的な値としてシードで持たせ、動的に再計算しない。投資信託と外貨預り金はもともと円建てなので逆算は不要。残りの2点も、履歴の単純appendが二重計上で壊れる話と、無人実行と書込前確認の矛盾だった。これも反映してもう一度Codexに通し、致命的指摘ゼロまで詰めた。3往復した。

スプレッドシートの形

新規に専用ファイルを1つ作り、タブで分ける方針にした。家族2人ぶんを同じファイルに同居させ、日次の履歴も同じファイルに溜める。

タブ役割
父 / 母保有銘柄1行ずつ+時価評価
履歴価格日付+人ごとの日次スナップショット(合計時価・損益・為替)
設定更新日時・採用為替などの共通メタ

保有タブの列は「人が手で編集する静的列」と「スクリプトが毎朝書き換える動的列」に分けた。静的列は種別・ティッカー・口座・価格ソース・通貨・保有数量・平均取得価格、それと崩れた前提から生まれた 取得原価(円) のシード。動的列は現在値・前日比・価格日付・評価額・為替・時価評価額(円)・評価損益(円)・評価損益(%)。参考として為替差益を除いた外貨ベースの値上がり率も別列に出す。

価格ソースはキーで汎用化した。stooq:<SYM>.US なら米国株、fund:<協会コード> なら投信、cash:USD なら外貨預り金。銘柄が増えても静的列にキーを書くだけで動的更新が効く。

履歴の二重計上を防ぐ

履歴タブの設計で一番神経を使ったのがここ。単純なappendは禁止にした。手動の /update-portfolio と朝の自動実行が同じ日に走れば二重計上するし、米国市場が休場でStooqが前日と同じ値を返したら、古い価格を当日分として積んでしまう。

なので履歴のキーを「実行日時」ではなく「価格の基準日+人」にした。書込前に最終行の価格日付を読み、取得した価格日付が前回より進んでいなければ履歴は追記しない。同じ価格日付+人がすでにあれば上書き(upsert)、なければ追記。Stooqが前回と同じ日付を返したら「休場 or 未更新」とみなしてスキップし、ログに残す。これで失敗した翌日に走っても、二重には積まれない。

書込権限の切り分け

「無人で毎朝書き込む」と「書込前にユーザー確認する」は普通は両立しない。ここは段階で割った。初回のシート作成・ヘッダ・初期データ投入・1回目の書込みだけは対話セッションで承認の上で実行し、確定した SPREADSHEET_ID を設定ファイルに固定保存する。以降のスクリプトは、その固定IDのファイル以外には絶対に書き込まない。書込範囲も決まった列・行だけ。タブ追加や列の増減みたいな構造変更は自動スクリプトでは一切やらず、対話セッションでだけ触る。これで無人実行でも事故りにくい。

処理の組み方

スクリプトは純粋関数と薄い副作用シェルに分けた。CSVをパースする関数、HTMLから基準価額を抜く関数、1銘柄ぶんの時価と損益を計算する関数、スプレッドシート書込用の2次元配列を組む関数。ここは引数だけ見て結果を返すので単体テストが書ける。fetchでStooq・みんかぶを叩く部分と、gwsでシートを読み書きする部分だけを外側のエントリに寄せた。

残したこと

設計書は memo/2026-06-03/rakuten-portfolio-tracker-design.md に置いた。実装はこれから。承認後にやる順番だけ並べておく。

  • 専用スプレッドシートを新規作成し、タブとヘッダを用意する
  • 確定した SPREADSHEET_ID を設定ファイルに固定保存する
  • 初期保有データと円建て取得原価のシードを投入する
  • update-portfolio.mjs を実装する(fetch→parse→compute→write、固定ID書込ガード、履歴upsert)
  • 純粋関数の単体テストを書く(休場日・古値スキップの境界を含める)
  • dry-runで楽天画面の時価評価額・評価損益と突合してから本書込みする
  • /update-portfolio skillを作る
  • タスクスケジューラに毎朝07:30で登録する
  • 1週間回して、履歴upsertと古値スキップが効いているか確認する

今日の収穫は、綺麗な計算式(為替×現在値で取得原価を出す)が現実とずれていたのをCodexに突かれて、検算して、設計を直したこと。AIが書いた計算ロジックでも、為替差益が消えるような落とし穴は人間が数字を見て拾わないと事故る。実装する前の設計段階で潰せたのは大きい。