第1回ではプラグインアーキテクチャ、第2回ではデータ層とパイプラインについて書いた。
第1回の時点では9本だったプラグインも、第2回で触れた通り14本に増えた。プラグインが増えるほど、矛盾する判定が増え、統合分析の価値が上がる。
最終回は、14本のプラグイン出力が Claude の手で統合レポートになり、Discord 経由でスマホに届くまでの仕組み。このシリーズで一番面白い部分だと思う。
実際に毎日16時に届く Discord 通知がこれ。

各プラグインの判定一覧に加えて、目標株価(計算値 + AI予測値)、矛盾の読み解き、推奨アクションまでが1つのレポートにまとまる
全体の流れ
14本のプラグイン実行
↓
各プラグインが PluginResult(signal / score / confidence / summary)を返す
↓
screener.py が consensus(BUY/SELL の数)と composite_score(加重平均)を計算
↓
上位銘柄の JSON を Claude Code CLI に渡す
↓
Claude が矛盾検出・推奨強度判定・レポート生成
↓
Discord Webhook で通知(毎日16時にスマホに届く)
Python が出すところまで:consensus と composite_score
Claude に渡す前に、Python 側で2つの指標を計算する。
consensus(合意)
@property
def consensus(self) -> dict:
buy_count = sum(1 for r in self.results if r.signal in {"STRONG_BUY", "BUY"})
sell_count = sum(1 for r in self.results if r.signal in {"STRONG_SELL", "SELL"})
neutral_count = len(self.results) - buy_count - sell_count
avg_score = sum(r.score for r in self.results) / len(self.results)
return {
"buy_count": buy_count,
"neutral_count": neutral_count,
"sell_count": sell_count,
"avg_score": round(avg_score, 3),
}
14本中何本が BUY を出して何本が SELL を出したか。シンプルだが、これが一番わかりやすい指標。
composite_score(加重平均スコア)
_SIGNAL_SCORE = {
"STRONG_BUY": 1.0,
"BUY": 0.75,
"NEUTRAL": 0.5,
"SELL": 0.25,
"STRONG_SELL": 0.0,
}
@property
def composite_score(self) -> float:
total = sum(
_SIGNAL_SCORE.get(r.signal, 0.5) * r.confidence
for r in self.results
)
return round(total / len(self.results), 4)
signal を数値化(STRONG_BUY=1.0 〜 STRONG_SELL=0.0)して confidence で重み付け。confidence が低いプラグイン(データ欠損など)の影響を自動的に抑える。
SELL フィルタ
composite_score でランキングする前に、SELL が過半数の銘柄は除外する。
# sell_count < len(results) // 2 + 1 の銘柄のみ通過
スコアが高くても半数以上のプラグインが SELL を出している銘柄は怪しい。一部のファクターだけ突出して高い「罠銘柄」を排除するためのフィルタ。
Claude に渡すもの:スリム化した JSON
スクリーニング上位の銘柄データを Claude に渡す。ただし、全フィールドを渡すとトークン消費が膨大になる。
_PLUGIN_FIELDS = {"plugin", "category", "signal", "score", "confidence", "summary", "target_price"}
# details フィールドを除いた軽量版を生成
slim_json = json.dumps(_slim(raw), ensure_ascii=False, indent=2)
各プラグインの details(手法固有の詳細データ:個別の財務指標やテクニカル値など)はカットして、判定結果(signal / score / confidence / summary)だけを渡す。1銘柄あたり約30行のコンパクトな JSON になる。
Claude に渡すのは「結論」であって「計算過程」ではない。この割り切りがトークン効率の鍵。
Claude Code CLI で統合分析
ここが RakuScan の一番ユニークな部分。Anthropic API(SDK)は使わず、Claude Code CLI を直接呼んでいる。
# src/notify/claude_analyzer.py
def analyze(screener_json: str) -> str:
cmd = ["claude", "-p", skill_content, "--output-format", "text"]
proc = subprocess.run(
cmd,
input=screener_json,
capture_output=True,
text=True,
timeout=180,
)
return proc.stdout.strip()
claude -p でプロンプトを渡し、stdin で JSON を流し込む。出力は Markdown のテキスト。API キーの管理が不要で、Claude Code のサブスクリプションで動くのがメリット。
なぜ API ではなく CLI か
- API キー管理が不要。個人開発のスクリプトにAPIキーを埋め込みたくなかった
-
Claude Code のスキル機能が使える。プロンプトを
.claude/skills/に Markdown ファイルとして管理できる - モデル選択が自動。Claude Code が最適なモデルを選んでくれる
デメリットはレスポンス速度(CLI起動のオーバーヘッド)と、Claude Code がインストールされた環境でしか動かないこと。個人開発の自分用ツールなので許容範囲。
プロンプト設計:矛盾検出がキモ
Claude に渡すプロンプト(スキルファイル)には、単なる「分析してください」ではなく、具体的な矛盾パターンと判断ルールを埋め込んでいる。
レジームチェック(最優先)
市場全体が下落トレンド(DEFENSIVE レジーム)のときは、どの銘柄も新規推奨しない。これは Claude に判断を任せるのではなく、プロンプト内で明示的にルール化している。
矛盾検出パターン
14本のプラグインが全部 BUY を出すことはまれで、むしろ矛盾する判定が出るのが普通。この矛盾をどう読み解くかが分析の価値。
プロンプトには5つの矛盾パターンを定義している。
| パターン | 解釈 |
|---|---|
| モメンタムBUY × 財務SELL | 勢いはあるが財務に不安。仕手株の可能性。見送り推奨 |
| バリューBUY × モメンタムSELL | 割安だが市場に無視されている。バリュートラップの可能性 |
| ファクター系BUY × ファンダ系BUY | 両方強い。信頼度が高い |
| テクニカルBUY × リスクSELL | トレンドは良いが損切り条件に近い。タイミング注意 |
| 全プラグインNEUTRAL | 明確なシグナルなし。様子見推奨 |
これを Claude にテンプレートとして渡すことで、「なんとなくそれっぽい分析」ではなく「定型パターンに基づく構造化された分析」を出力させている。
推奨強度の4段階判定
buy_count と avg_score の組み合わせで、推奨の強さを4段階に分ける。
- 強い推奨: buy_count が多く avg_score が高い
- 推奨: buy_count が過半数
- 弱い推奨: buy_count はあるが avg_score が低い
- 見送り: buy_count が少ない、または矛盾が多い
Claude にはこの4段階を使ってレポートを書くよう指示している。
さらに、目標株価も2種類算出させている。プラグインの数値から機械的に計算した「計算値」と、Claude がセクター特性やリスクを織り込んで調整した「AI予測値」。先ほどのスクリーンショットでは、計算値4,503円(+78.8%)に対してAI予測値が3,800円(+51%)と出ている。資源株のボラティリティを考慮して下方修正した、という根拠も添えられている。
Discord への通知
Webhook(一方通行の自動通知)
日次スクリーニングの結果は Discord Webhook で送信する。
# src/notify/discord.py
def send_claude_analysis(analysis: str, total_screened: int, as_of: str = "") -> None:
payload = {
"embeds": [{
"title": "🤖 Claude 統合分析レポート",
"description": analysis, # Markdown テキスト(最大4000文字)
"color": 0x2ECC71, # 緑
"footer": {
"text": f"RakuScan • {as_of} • {total_screened}銘柄スクリーニング済"
},
}]
}
requests.post(webhook_url, json=payload)
Claude の分析が取得できなかった場合(タイムアウト等)は、従来形式のスクリーニング結果をフォールバック通知する。Claude が落ちてもパイプライン自体は止まらない。
シグナルの視覚化
Discord Embed のカラーコードとシグナル絵文字で、パッと見で判断できるようにしている。
_SIGNAL_EMOJI = {
"STRONG_BUY": "🟢🟢",
"BUY": "🟢",
"NEUTRAL": "⚪",
"SELL": "🔴",
"STRONG_SELL": "🔴🔴",
}
_COLOR_BUY = 0x2ECC71 # 緑
_COLOR_SELL = 0xE74C3C # 赤
_COLOR_DEFENSIVE = 0xF39C12 # オレンジ(守りレジーム)
スマホの通知を開いた瞬間に「今日は買い推奨が出てるな」「守りモードだな」がわかる。
双方向 Discord Bot
Webhook は一方通行。気になる銘柄をその場で深掘りしたいときのために、双方向の Discord Bot も動かしている。
コマンド体系
実際に !analyze 2760(東京エレクトロン デバイス)を実行した画面がこちら。

14本のプラグインが一斉に走り、シグナル(🟢🟢/🟢/⚪/🔴)・スコア・サマリーが一覧で返ってくる。レジーム・合意・composite_score まで一目でわかる
即レス系(Claude呼び出しなし):
| コマンド | 機能 |
|---|---|
!scan |
スクリーニング実行 |
!analyze 7203 |
指定銘柄を全プラグインで分析 |
!portfolio |
ポートフォリオ表示 |
!performance |
週次パフォーマンス |
!regime |
市場環境判定 |
Claude経由系(レート制限あり):
| コマンド | 機能 |
|---|---|
!research 半導体セクターの見通し |
Claude にリサーチ質問 |
!why 7203 |
判断根拠の説明 |
!compare 7203 6758 |
2銘柄の比較分析 |
レート制限
Claude を呼ぶコマンドにはレート制限をかけている。
class RateLimiter:
def __init__(self, per_hour: int = 10, per_day: int = 30, cooldown_seconds: int = 30):
self._per_hour = per_hour
self._per_day = per_day
self._cooldown = cooldown_seconds
1時間あたり10回、1日あたり30回、コマンド間隔30秒。Claude Code CLI の呼び出しコスト(時間・トークン)を考慮した設定。
Claude の非同期呼び出し
Discord Bot は discord.py の非同期フレームワークで動いているので、Claude Code CLI の呼び出しも非同期で行う。
async def _call_claude(prompt: str, stdin_data: str = "") -> str | None:
cmd = ["claude", "-p", prompt, "--output-format", "text"]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
proc.communicate(stdin_data.encode()),
timeout=180,
)
return stdout.decode().strip()
タイムアウトは180秒。Claude が考え込んでも Bot が固まらない。
セキュリティ
Bot にはチャンネル制限とユーザー制限をかけている。
class RakuScanBot(discord.Client):
def __init__(self, bot_config: dict):
self._allowed_channels: set[int] = {int(c) for c in bot_config.get("allowed_channels", [])}
self._allowed_users: set[int] = {int(u) for u in bot_config.get("allowed_users", [])}
指定チャンネル・指定ユーザー以外からのコマンドは無視する。投資判断に関わるシステムなので、第三者がコマンドを打てない状態にしている。
運用の全体像
平日の運用フローをまとめるとこう。
16:00 cron → daily_run.py 起動
16:01 ユニバース取得 + データプリフェッチ(キャッシュ活用で10秒以内)
16:02 14本のプラグインが200銘柄を分析(キャッシュヒットで高速)
16:03 上位銘柄の JSON → Claude Code CLI → 統合レポート生成
16:05 Discord Webhook でスマホに通知
随時 気になったら Discord Bot に !analyze 7203 を打つ
→ 全プラグイン実行 → 結果を Embed で返答
土曜 weekly_run.py → 事後検証 + パフォーマンスレポート
月初 monthly_run.py → ユニバース再構築
スマホの通知を見て、気になる銘柄があれば Bot で深掘りする。この「自動通知 + 対話的な深掘り」の組み合わせが、使っていて一番便利なポイント。
シリーズまとめ
3回にわたって RakuScan の設計を書いてきた。
| 回 | テーマ | キーワード |
|---|---|---|
| 第1回 | プラグインアーキテクチャ | PluginResult / YAML動的ロード / 1ファイル=1手法 |
| 第2回 | データ層とパイプライン | 3つの無料API / ABC抽象化 / 2層キャッシュ / 月次・週次・日次 |
| 第3回(本記事) | Claude統合とDiscord | Claude Code CLI / 矛盾検出プロンプト / Webhook + Bot |
設計思想をひとことでまとめると「Python で定量、Claude で統合、Discord で通知」。
確定的に計算できるものは Python で回し、複数の矛盾する判定を統合して人間に伝える部分だけを Claude に任せる。この役割分担がうまくハマると、LLM のコストを抑えつつ、再現性のある分析が回せる。
個人開発でも、API の無料枠とキャッシュ戦略を工夫すれば、200銘柄の日次スクリーニングシステムは十分に運用できる。
このシリーズの内容をさらに深掘りした有料本を公開中。各プラグインの戦略設計の詳細、設計判断の裏側にある思想、失敗と修正の過程、実コードの解説など、連載では書ききれなかった内容を15章・約5万文字にまとめた。
https://zenn.dev/sktt_panda/books/rakuscan-design-philosophy
この記事は Zenn にも同じ内容を投稿しています。
