claude.aiスケジューラからローカルlaunchdへ移行した話 — yfinance 403エラーの解決

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だけで動いており、障害点が少なく見通しがよい。