3行まとめ
- RakuScan(自分用の日本株スクリーニングツール)は、複数の分析プラグインの判定結果をClaudeが統合してBUY/HOLD/SELLの最終判断を出す設計。だが「Claudeが複数の判定を混ぜること自体に価値があるのか」を確認する手段が最初はなかったので、専用の自動検証システムを作った
- シグナル発行後+1/3/5/10/20営業日のリターンを
evaluate.pyが自動記録し、週次でweekly_report.pyが「Claudeの正解率」と「プラグイン平均の正解率」を突き合わせてvs_plugin_avg_5dという指標に落とす。Claudeのconfidence(自信度)が実際の的中と相関しているかも同時にチェックする設計にした - 直近10週(2026-W26〜W35)の実データでは、Claudeの正解率がプラグイン平均と同水準の週もあれば下回る週もあり、この期間で安定して上回った週は一度もないという結果になっている。数字を良く見せることではなく、判断の質を継続的に監視できる仕組みを作ったこと自体が今回の要点
RakuScanは複数の分析プラグイン(モメンタム・バリュー・財務健全性など)の判定を集約して銘柄をスクリーニングし、最終的にはClaudeがそれらを統合してBUY/HOLD/SELLの判断とレポートを生成する。ここでは投資判断そのものではなく、「AIに複数の判定を統合させる」という設計が実際に機能しているかを継続的に検証する仕組みの話を書く。
なぜ検証の仕組みが必要だったか
Claudeに複数のプラグイン結果を渡して統合判断させる設計にした時点で、素朴な疑問が残っていた。Claudeが判定を混ぜることで、本当に個々のプラグインより良い判断になっているのか。「AIに任せればうまくいくはず」という感覚だけで運用するのは危うい。個々のプラグインは学術論文ベースの手法で勝率を機械的に計算できるが、Claudeの統合判断がそれらの平均より優れているかどうかは、実際に事後検証してみるまで分からない。
そこで、シグナルを発行して終わりにするのではなく、発行後に実際の株価推移と突き合わせて答え合わせをする仕組みを組み込んだ。
テーブル設計 — シグナルと検証結果を分離する
DBスキーマはシグナル発行時の記録と、事後検証の結果を別テーブルに分けている(この記事に関係する主要カラムのみ抜粋)。
-- シグナル発行時に記録
CREATE TABLE signal_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
signal_date TEXT NOT NULL,
signal_type TEXT, -- "BUY" / "SELL" / "NEUTRAL"
plugin_results TEXT NOT NULL, -- 各プラグインの個別判定(JSON)
claude_verdict TEXT NOT NULL, -- "BUY" / "HOLD" / "SELL"
claude_confidence REAL, -- 0.0〜1.0
price_at_signal REAL NOT NULL,
evaluated INTEGER NOT NULL DEFAULT 0
);
-- 事後検証結果(evaluate.pyが追記)
CREATE TABLE signal_evaluation (
signal_id INTEGER REFERENCES signal_log(id),
eval_days INTEGER NOT NULL, -- 1, 3, 5, 10, 20
eval_date TEXT NOT NULL,
price_at_eval REAL NOT NULL,
return_pct REAL NOT NULL,
is_correct BOOLEAN,
PRIMARY KEY (signal_id, eval_days)
);
シグナル発行時のsignal_logにはプラグイン個別の判定結果とClaudeの統合判断・confidenceをまとめて保存し、signal_evaluationは完全に事後生成のテーブルとして分離した。こうしておくことで、シグナル発行ロジックに一切手を入れずに、検証ロジック側だけを何度でも作り直せる。
evaluate.py — 営業日ベースで事後検証する
scripts/evaluate.pyは、evaluated=0の未検証シグナルを取得し、+1/3/5/10/20営業日後の株価を取得してリターンを計算する。
_EVAL_HORIZONS = [1, 3, 5, 10, 20]
def _add_business_days(start: date, n: int) -> date:
"""start から n 営業日(月〜金)後の日付を返す。"""
result = pd.bdate_range(start=start, periods=n + 1, freq="B")
return result[-1].date()
def _get_price_on_or_after(symbol: str, target_date: date, window_days: int = 5) -> float | None:
"""target_date 以降の最初の終値を返す。"""
end = target_date + timedelta(days=window_days)
records = get_prices(symbol, target_date.isoformat(), end.isoformat())
for r in records:
if r.date >= target_date.isoformat():
return r.close
return None
pd.bdate_rangeでカレンダー日ではなく営業日ベースの日付を計算しているのがポイント。土日を挟むシグナルでも「+5営業日後」が正しく計算される。評価日の株価が休場や欠損で取得できない場合はwindow_days分だけ後ろにずらして探索し、それでも見つからなければその回はスキップして次回の実行に持ち越す。
正解判定はシンプルで、BUYシグナルならリターンが正なら正解、SELLシグナルなら負なら正解、NEUTRALは判定なし。
if signal_type == "BUY":
is_correct: int | None = 1 if return_pct > 0 else 0
elif signal_type == "SELL":
is_correct = 1 if return_pct < 0 else 0
else:
is_correct = None # NEUTRAL は正解判定なし
全ホライゾン(5種類)の記録が揃ったシグナルだけsignal_log.evaluatedを1に更新し、次回以降の処理対象から外す。
weekly_report.py — プラグイン別とClaude統合判断を同じ土俵で比較する
週次バッチのscripts/weekly_report.pyは、その週に確定した評価結果を集計して2種類の成績を出す。ひとつはプラグイン個別の正解率(plugin_performanceテーブル)、もうひとつはClaudeの統合判断の正解率(claude_performanceテーブル)。
Claude側の集計はこういうクエリで取る。
rows = db.execute(
"""
SELECT
sl.claude_verdict,
sl.claude_confidence,
MAX(CASE WHEN se.eval_days = 5 THEN se.is_correct END) AS correct_5d,
MAX(CASE WHEN se.eval_days = 20 THEN se.is_correct END) AS correct_20d
FROM signal_log sl
JOIN signal_evaluation se ON sl.id = se.signal_id
WHERE se.eval_days IN (5, 20)
AND sl.signal_date BETWEEN ? AND ?
GROUP BY sl.id, sl.claude_verdict, sl.claude_confidence
""",
(week_start, week_end),
)
そして、Claudeの正解率とプラグイン平均正解率の差をvs_plugin_avg_5dとして保存する。
acc_5d: float | None = claude_stats.get("accuracy_5d")
vs_plugin: float | None = (
round(acc_5d - plugin_avg_accuracy_5d, 4)
if acc_5d is not None and plugin_avg_accuracy_5d is not None
else None
)
比較対象のplugin_avg_accuracy_5dは、その時点で有効化されているプラグインだけに絞って計算している。無効化中のプラグインをシャドー実行で裏側に走らせている仕組み自体は別記事で書いたが、Claudeが実際に目にしているのは有効化中のプラグインの結果だけなので、比較もそこに揃えている。
enabled_map = _current_enabled_map()
plugin_accs = [
s["correct_5d"] / s["total_5d"]
for name, s in stats.items()
if s["total_5d"] > 0 and enabled_map.get(_yaml_plugin_name(name), False)
]
plugin_avg_acc = sum(plugin_accs) / len(plugin_accs) if plugin_accs else None
vs_plugin_avg_5dがプラスなら「Claudeの統合判断はプラグイン単体の平均より当たっている」、ゼロやマイナスなら「統合する意味が薄い、あるいは足を引っ張っている」ことになる。
confidenceのキャリブレーションも見る
もうひとつ組み込んだのが、Claudeが申告するclaude_confidenceが実際の的中と相関しているかのチェック。正解したときの平均confidenceと、外したときの平均confidenceを別々に集計する。
conf_correct = [
r["claude_confidence"] for r in rows
if r["correct_5d"] == 1 and r["claude_confidence"] is not None
]
conf_wrong = [
r["claude_confidence"] for r in rows
if r["correct_5d"] == 0 and r["claude_confidence"] is not None
]
理屈上は「正解したときのconfidenceの方が、外したときより高い」となってほしい。この差が小さい、あるいは逆転しているなら、confidenceの数値が判断の確からしさを反映していないということになる。数値さえ出していれば信頼できるとは限らない、という前提でチェック項目に加えた。
実際の10週間のデータ
2026年W26〜W35のclaude_performanceテーブルの実データが以下。
| 週 | シグナル数 | Claude正解率(5d) | プラグイン平均 | 差分 | 正解時conf | 誤り時conf |
|---|---|---|---|---|---|---|
| W35 | 23 | 0.609 | 0.609 | 0.000 | 0.568 | 0.574 |
| W34 | 65 | 0.338 | 0.338 | 0.000 | 0.590 | 0.600 |
| W33 | 66 | 0.409 | 0.409 | 0.000 | 0.598 | 0.606 |
| W32 | 107 | 0.692 | 0.692 | 0.000 | 0.638 | 0.609 |
| W31 | 93 | 0.237 | 0.237 | 0.000 | 0.598 | 0.596 |
| W30 | 75 | 0.280 | 0.302 | -0.022 | 0.515 | 0.521 |
| W29 | 99 | 0.222 | 0.263 | -0.041 | 0.517 | 0.527 |
| W28 | 132 | 0.205 | 0.228 | -0.024 | 0.521 | 0.532 |
| W27 | 160 | 0.200 | 0.226 | -0.026 | 0.531 | 0.533 |
| W26 | 195 | 0.328 | 0.345 | -0.017 | 0.535 | 0.537 |
見ての通り、この10週間では差分がプラスになった週が一度もない。ゼロ(プラグイン平均と同水準)かマイナスに寄っている。なお、W31〜W33はplugin_performanceに記録されたプラグイン数が通常週(14〜16本)に対して2本と極端に少なく、しかもその2本の正解率がClaudeの正解率と完全一致していた。サンプルが薄いというより「相関1のプラグイン2本の平均とClaudeを比べているだけ」に近い状態なので、この3週の差分(いずれも0.000)はそのまま真に受けず参考程度に見ておきたい。
もうひとつ気になるのが正解時confidenceと誤り時confidenceの差。10週のうち8週(W26〜W30、W33〜W35)で、誤ったときのconfidenceの方がわずかに高いという、理屈上望ましくない逆転が起きている。正解時の方が高かったのはW31・W32の2週のみ。差自体は0.01前後とごく小さいが、「Claudeの自信度の高さが的中率の高さを保証しない」ことを示す結果になっている。
まとめ
- 「複数プラグインの判定をClaudeに統合させる」という設計判断が実際に機能しているかを、感覚ではなく
vs_plugin_avg_5dという数値で継続的に監視できる仕組みを作った - 直近10週の実データでは、Claudeの統合判断がプラグイン平均を安定して上回っているとは言えず、confidenceも的中率とはっきり相関しているとは言えない結果になっている
- AIに複数の判断材料を統合させる設計は一見もっともらしいが、「本当に価値を足しているか」は作った側が自分で疑って検証する仕組みを持たないと分からない。この考え方は投資分析に限らず、複数のシグナルをLLMに統合判断させる構成全般に応用できる
RakuScanのプラグインアーキテクチャ・データ層設計・Claudeによる統合レポート生成については、以下の連載・有料本でさらに詳しく解説している。
https://zenn.dev/sktt_panda/books/rakuscan-design-philosophy
この記事は Zenn にも同じ内容を投稿しています。