claude.aiスケジューラからローカルlaunchdへ移行した話 — yfinance 403エラーの解決
発端:毎朝のルーティンがサイレントに失敗していた
このブログの分析レポートは、claude.aiのスケジューラ(CCR: Claude Code Remote)が毎朝自動実行することで生成されている。ある時点から、レポートが生成されなくなった。ログを確認すると、以下のエラーが記録されていた。
Failed to get ticker '^N225' reason: Failed to perform,
curl: (56) CONNECT tunnel failed, response 403.
日経平均(^N225)のデータ取得がブロックされていた。yfinanceを使った外部API接続が、Claude Codeのリモート実行環境から行われる際に403で弾かれていたのだ。
切り分け:ローカルでは動く、リモートでは動かない
まずターミナルから直接Pythonを実行してみた。
from data.market_summary import MarketSummary
ms = MarketSummary()
print(ms.generate_text())
問題なく日経平均・ドル円・VIXなどのデータが取得できた。つまりyfinance自体の問題ではなく、実行環境の問題だと分かった。
CCRはAnthropicのクラウドインフラ上でClaudeセッションを実行する仕組みである。このインフラからの外部API接続(Yahoo Finance等)がプロキシによってブロックされていた。ローカルのターミナルから実行すれば問題ないが、CCR経由では同じコードが403を返す。
当初の解決案:データ取得だけを切り離す
最初に考えたのは「データ取得だけをローカルで先に実行し、JSONに保存しておく」というアプローチだった。
ローカル launchd(先行実行)
└─ update_market_data.py → yfinanceでデータ取得 → JSONに保存
CCRスケジューラ(後続実行)
└─ JSONを読むだけ → 分析・レポート生成
ただしこの構成は複雑で、ローカルとリモートの実行タイミング依存が生まれる。試作したが、すぐに削除した。
採用した解決策:全てローカルに移行する
シンプルな結論に至った。CCRでやっていることを全てローカルのlaunchdで実行すれば良い。
CCRが不要になる理由は明確だった。
- CCRが失敗する根本原因はネットワーク制限であり、回避策は複雑になる
- ローカル実行であれば制限が一切ない
- すでに
morning_routine_jp.shというシェルスクリプトが存在しており、各スキルをclaude --printで順番に実行する仕組みが実装済みだった - 各スキルが独立したセッションで実行されるため、CCRで問題になっていたコンテキスト蓄積(トークン上限)も発生しない
CCRとの違いは「Macが起動している必要がある」という点だけだ。朝7時台にMacが起動していれば問題ない。
移行作業の内容
launchdジョブの整理
移行前、~/Library/LaunchAgents/には使われていないplistファイルが複数残っていた。
| ファイル | 問題 |
|---|---|
com.tradeapp.morning.plist |
morning_routine_jp.shを8:37に実行する古いファイル。新しいmorning-routine-jp(7:37)と重複 |
com.tradeapp.evening.plist |
削除済みのmorning_routine_us.shを参照していた |
com.tradeapp.evening.plist.backup |
backupファイルがそのまま残存 |
com.tradeapp.market-updater |
試作したupdate_market_data.pyのplistがアンロードされずに残存 |
これらを全てアンロード・削除した。
スクリプトの修正
morning_routine_jp.shに2点追加した。
1. git pull(開始時)
CCRは実行開始時にgit pullでmemory/の最新状態を取得していた。ローカル実行でも同様に、他の端末から更新された内容を取り込むために必要だ。
2. git push(終了時)
各スキルがmemory/にレポートを書き出した後、GitHub ActionsがGhost投稿をトリガーするためにgit pushが必要だ。
Ghost下書きの保存先変更
従来のmorning_routine_jp.shはGhost下書きを/tmp/に保存してghost_poster.pyをローカル実行していた。しかしGitHub Actionsを活用する構成では、memory/ghost_draft_jp_{日付}.mdに保存してgit pushするだけで自動投稿が走る。この方が一貫性があるため、保存先を変更した。
PTSスキャンのローカル化
夕方16:30に実行されるPTS急騰スキャンも同様にローカル化した。CCRのスケジューラに設定していたプロンプトをそのままpts_scan.shに移植し、launchdで16:30に実行するようにした。
移行後の構成
ローカル Mac(launchd)
├─ morning_routine_jp.sh 月・水・金 7:37
│ git pull → market → deep-dive → recommend
│ → health-check → Ghost下書き → git push
│
├─ discover_jp.sh 火曜 8:44
│ 未発掘株スキャン → Ghost下書き(会員制)
│
└─ pts_scan.sh 平日 16:30
PTS急騰スキャン → memory保存 → git push
GitHub Actions
└─ ghost_draft_*.md の push を検知 → Ghost自動投稿
└─ dlvr.it → X(Twitter)自動転送
CCRのスケジューラは2つとも停止した。
副次的な整理
移行の過程で、使われていないファイルも一括して削除した。
blog_writer.sh/blog_writer.py(中学生向けブログ記事生成)tiktok_poster.sh/tiktok_poster.py(TikTok投稿)morning_routine_us.sh(米国株ルーティン、トークン上限問題により停止中)- 関連するlaunchdジョブ(autotrade-jp/us、blog-writer)
まとめ
CCRのネットワーク制限という問題が、システム全体の簡素化につながった。複雑な回避策を積み重ねるより、シンプルな構成に戻す方が長期的に安定する。現在のシステムは3つのlaunchdジョブとGitHub Actionsだけで動いており、障害点が少なく見通しがよい。