免責事項
本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。CAN-SLIM C×A 複合判定の閾値(C: 四半期 EPS YoY +25%、A: 5年 CAGR +25% + 連続増益3年)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。本記事中に J-Quants API・FMP(Financial Modeling Prep)から取得した実データ・株価実値は掲載していません(利用規約に基づく方針)。CAN-SLIM はオニール(William J. O’Neil)著「How to Make Money in Stocks」で提唱された手法を基にエンジニア視点で再構築しています。
記事#22では CAN-SLIM の C(直近1四半期の急成長)、記事#23では A(過去3-5年の長期持続成長)を Python で個別に自動判定しました。本記事はその交差点—— C×A を複合判定し、「直近加速 + 長期持続」両方を満たす成長株候補の最有力ゾーンを Python で自動抽出する手順とサンプルコード入門です。発展編 CAN-SLIM 実装ブロック(#22→#23→#24)の完結回です。
「絶対値より変化率、突発より連続性」という設計原則を、本記事は多軸 AND 判定に拡張します。C 単独 PASS だけだと「直近1Q だけ突発」、A 単独 PASS だけだと「過去5年は良いが直近失速」の可能性があります。両軸 PASS の交差点でようやく「成長株候補の入口」と言える、というのが CAN-SLIM 原典の核心思想です(ただし C×A は CAN-SLIM 7要素中2要素なので、これだけで「本物」と断ずるのは原典より強い表現になります)。
多くの読者がぶつかる壁:
- 戦略選択: AND(両方 PASS) / OR(どちらか PASS) / 重み付け(C 60% + A 40% など)の3戦略から自分の運用にどう選ぶか
- 9セルマトリクス解釈: C×A の verdict を 3×3 マトリクスで集計したとき、各セル(PASS×PASS / PASS×CAUTION / FAIL×PASS など)の意味と運用上の扱い
- 業種分散: C×A 両 PASS 銘柄が IT 業種に集中しがちで、応用編 #19 HHI 集中度判定との連携が必要
- バックテスト: 過去 C×A 両 PASS だった銘柄のその後の株価推移を簡易検証(実値非掲載の概念実装)
- ポートフォリオ運用: PASS 銘柄を何銘柄、どう保有するか(オニール原典の経験則に従う運用ルール)
筆者は製造業の研究開発現場で、新製品の量産化決定を「品質 × 性能 × コスト」の3軸 AND 判定で行ってきました。単軸の高得点で量産投資して失敗した過去 → 3軸 AND 判定で成功率を上げた経験は、CAN-SLIM の C×A 複合判定と同じ枠組みです。本記事のエピソードで詳しく整理します。
本記事で扱う専門用語の予習
- 複合判定(Composite judgment): 複数の独立判定軸(C, A)を組み合わせて1つの最終判定を出す方式。AND, OR, 重み付け平均の3戦略
- 9セルマトリクス: C 判定(PASS/CAUTION/FAIL)× A 判定(PASS/CAUTION/FAIL)の3×3 = 9マス。各マスの銘柄数分布が運用判断の出発点
- HHI(Herfindahl-Hirschman Index): 業種集中度。応用編 #19 で導入、シェア二乗和。閾値 2000/3000 で警告
- バックテスト: 過去のデータで戦略の有効性を検証する手法。本記事は概念実装のみ(実値非掲載)
- 勝率(Win rate): バックテストで PASS 銘柄が一定期間後に株価上昇していた割合。バックテストで 50-55% が標準、65% を超える結果は過剰適合の疑い
- 偽陽性(False positive): PASS 判定したが実際は成長失速した銘柄。複合判定で減らしたい誤判定
- ピーク EPS 型失速: PASS 判定時点が EPS 成長のピーク直前で、判定後に成長率が急減速するパターン(Zoom 2021、Peloton 2022、半導体ピーク 2021Q4 などの典型例)
「実装したくない読者」向け代替案
「複合判定を自分で実装するのは大変」という読者には、米国 IBD の “Composite Rating”(C/A/N/S/L/I/M を内部で複合した0-99スコア)、マネックス証券の銘柄スクリーナー複数条件AND、SBI証券の Morningstar スター評価で同等機能が利用可能です。本記事の自動判定は「自分の閾値・戦略で日米両市場をスクリーニングしたい」用途で、結果だけ見たい読者には不要です。
本記事の前提と難易度
- 必須前提: #22(C 要素)と #23(A 要素)の実装、または同等の四半期/年間 EPS データが
quarterly_eps_with_yoyとannual_eps_with_cagrテーブルに揃っている状態 - pandas(
merge、groupby、pivot_table)と matplotlib(ヒートマップ)の基本 - 動作環境: Python 3.11+ / pandas 2.x / numpy / matplotlib 3.9+ / duckdb 1.0+
- 戦略軸 vs 戦術軸: 発展編 #21 戦略軸の延長として、#22-#24 が戦術軸(C、A、C×A)。本記事 #24 で戦術軸の集大成、#25 以降は事業構造分析(製品アーキテクト視点)に進む
成長株投資 固有のリスク(複合判定でも残る)
C×A 両 PASS でも以下のリスクは残ります(#23 提示の継承 + 拡張):
- 成長失速リスク(ピーク EPS 型失速): 過去 C×A PASS でも、市場飽和や競合参入で翌年に成長が一桁台に落ちる典型例:
- Zoom Video 2021型: コロナ特需後の急減速、PASS 判定 → 数四半期で50%以上の株価下落
- Peloton 2022型: 巣ごもり需要剥落、長期 CAGR 高 + 直近 YoY 高 → 翌期から需要消失
- 半導体ピーク 2021Q4型: シクリカル業種の循環ピーク、CAN-SLIM C×A だけでは循環性を捉えられない
- 高 PER リスク: 成長株は PER 30倍以上が常態。市場予想を下回ると30-50%急落
- 業種集中リスク(複合判定で顕在化): C×A 両 PASS 銘柄は IT・半導体・ヘルスケアに極端に偏る傾向。応用編 #19 HHI でチェック推奨
- 為替リスク: 米国株の円換算リターンが為替変動でぶれる
- GAAP/Non-GAAP の影響: 米国テック成長株は GAAP だと長期赤字 → 全銘柄 CAUTION 化の構造(#23 で提示)
応用編 #16 安定性指標 + #19 業種分散 + 本記事 C×A 複合判定の3層フィルタが実装上の対策です。後段「ポートフォリオ運用ルール」で具体的なリスク管理ルールも提示します。
ポートフォリオ運用ルール(オニール原典の経験則、I1 反映)
C×A AND PASS 銘柄を抽出した後の運用は、原典で次のルールが推奨されています:
- 保有銘柄数の上限: 5-8銘柄(個人投資家の経験則。多すぎると管理が分散、少なすぎると個別株リスク)
- 1銘柄あたりの上限比率: ポートフォリオの 20-25%(最大集中時)、初期エントリーは 5-10%
- 損切ライン: エントリー価格から -7〜-8%(オニール原典のハードルール、例外なく適用)
- 利確ライン: +20〜25%で部分利確(カップ&ハンドル等のチャートパターンで延長判断)
- 追加買付け(ピラミッディング): 初期エントリー後 +2.5% で1回目、+5% で2回目の追加買付け(強気局面)
本記事の自動判定は「銘柄候補のスクリーニング」で、ここから先のエントリー判断・運用は別フェーズです。CAN-SLIM はオニール原典でも「7要素中過半数を満たし、市場全体(M要素)が強気」局面でのみ機能する手法であり、機械 PASS = 即購入候補ではない点に注意してください。
本記事では 4個のサンプルコードで、C×A 複合判定関数(3戦略)→ screen_parallel_v3 統合 → 9セル マトリクス可視化 → 簡易バックテスト概念実装まで自動判定を完成させます。
結論:CAN-SLIM C×A 複合判定は「直近加速(C)と長期持続(A)の両軸 PASS」を AND 戦略で機械的に自動判定する。複合戦略は AND(両方 PASS、最も厳格)/ OR(どちらか PASS、緩い)/ 重み付け(C 60% + A 40% などスコア化)の3パターン。9セルマトリクスで verdict 分布を可視化し、業種分散(応用編 #19 HHI)と組合せて偽陽性を削減。「絶対値より変化率、突発より連続性」原則を多軸 AND 判定に拡張するのが本記事の技術的核心。発展編 CAN-SLIM 実装ブロック(#22→#23→#24)はこの記事で完結し、#25 以降は事業構造分析に進む。重要:本記事の C×A 複合判定は CAN-SLIM 7要素中2要素のみで、機械 PASS = 即購入候補ではなく「成長株候補の最有力ゾーン」のスクリーニング手段です。エントリー判断・損切・利確は原典のポートフォリオ運用ルールに従ってください。
C×A 複合判定の役割と「両軸 PASS」が示す成長株候補の最有力ゾーン
CAN-SLIM 原典では C と A は独立に評価されますが、成長株候補の最有力ゾーンは両軸を同時に満たすのが経験則です。各組合せの解釈:
9セルマトリクスの意味(C × A の verdict 組合せ)
- PASS × PASS(最有力): 直近加速 + 長期持続、成長株候補の入口(米国 S&P500 の概ね 1-3%、東証プライム 0.5-1%)
- PASS × CAUTION: 直近強気だが長期データ不足 or 中庸成長。新規上場の急成長銘柄に多い、CAUTION 扱い
- PASS × FAIL: 直近1Q だけ突発、長期は微成長以下。一過性の可能性大、FAIL 寄り
- CAUTION × PASS: 長期実績はあるが直近失速、成熟期入りのシグナル
- CAUTION × CAUTION: 両軸とも中庸、ニュートラル
- CAUTION × FAIL / FAIL × CAUTION / FAIL × FAIL: 成長株フレームワークでは除外
3戦略の選択(運用スタイル別)
- AND 戦略(推奨デフォルト): C PASS かつ A PASS のみ最終 PASS。最も厳格、偽陽性低、抽出数少(米国 1-3%、日本 0.5-1%)
- OR 戦略: C か A どちらかが PASS。抽出数多いが偽陽性も多い、教育目的・候補リスト用
- 重み付け戦略: C verdict と A verdict を数値化(PASS=2, CAUTION=1, FAIL=0)して重み付け平均、閾値で判定。柔軟だが恣意性あり
本記事は AND 戦略をデフォルトに、引数で OR / 重み付けに切替可能な設計にします。「絶対値より変化率、突発より連続性」原則は AND 戦略で最も強く担保されます。
スニペット1:C×A 複合判定関数のサンプルコード入門(AND / OR / 重み付け 3戦略)
#22 の judge_c_element と #23 の judge_a_element の出力を受け取り、3戦略から1つを選んで複合判定する関数を実装します。引数 strategy で “and” / “or” / “weighted” を切替可能。
# can_slim_composite_judge.py — C×A 複合判定(3戦略、自動判定の中核)
# 動作環境: Python 3.11+
from typing import Literal, TypedDict, NotRequired
Verdict = Literal["PASS", "CAUTION", "FAIL"]
Strategy = Literal["and", "or", "weighted"]
# verdict を数値化するスコアマップ(重み付け戦略で使用)
_VERDICT_SCORE = {"PASS": 2, "CAUTION": 1, "FAIL": 0}
class CompositeMetrics(TypedDict):
c_verdict: NotRequired[Verdict | None]
a_verdict: NotRequired[Verdict | None]
def judge_composite(metrics: CompositeMetrics,
strategy: Strategy = "and",
c_weight: float = 0.6,
a_weight: float = 0.4,
weighted_pass_threshold: float = 1.6,
weighted_caution_threshold: float = 1.0) -> Verdict:
"""C×A 複合自動判定(突発より連続性 + 多軸 AND の原則)
strategy="and"(推奨デフォルト、両方 PASS のみ最終 PASS)
strategy="or"(緩い、どちらか PASS で最終 PASS)
strategy="weighted"(C/A を重み付け平均してスコア化、閾値で判定)
weighted_pass_threshold=1.6: PASS×PASS=2.0, PASS×CAUTION=1.6, PASS×FAIL=1.2 など
"""
c = metrics.get("c_verdict")
a = metrics.get("a_verdict")
if c is None or a is None:
return "CAUTION"
if strategy == "and":
if c == "PASS" and a == "PASS":
return "PASS"
if c == "FAIL" or a == "FAIL":
return "FAIL"
return "CAUTION"
if strategy == "or":
if c == "PASS" or a == "PASS":
return "PASS"
if c == "FAIL" and a == "FAIL":
return "FAIL"
return "CAUTION"
# weighted strategy
if strategy == "weighted":
score = _VERDICT_SCORE[c] * c_weight + _VERDICT_SCORE[a] * a_weight
if score >= weighted_pass_threshold:
return "PASS"
if score >= weighted_caution_threshold:
return "CAUTION"
return "FAIL"
raise ValueError(f"Unknown strategy: {strategy}")
# 使用例
# from can_slim_c_judge import judge_c_element
# from can_slim_a_judge import judge_a_element
#
# c_v = judge_c_element({"yoy_growth_pct": 30.0, "consecutive_up_quarters": 3})
# a_v = judge_a_element({"cagr_5y_pct": 28.0, "consecutive_up_years": 4})
# composite = judge_composite({"c_verdict": c_v, "a_verdict": a_v}, strategy="and")
# print(composite) # "PASS" if both PASS, else CAUTION/FAIL
エンジニア的に言い換えると(多軸 AND 判定の品質管理作法)
本コードは、製造業の品質管理で言う 「多軸 AND 判定」の作法と重なります。製品の量産化判定でも「品質 PASS かつ 性能 PASS かつ コスト PASS」の AND が原則で、いずれか1軸でも FAIL なら量産投資を保留します。AND 戦略は偽陽性を最小化する一方で抽出数が減るトレードオフがあり、運用スタイルに応じて OR/ 重み付けに切り替える設計が strategy 引数の役割です。「絶対値より変化率、突発より連続性」原則は AND 戦略で最も強く担保されます。
スニペット2:screen_parallel_v3 への統合と 9セルマトリクス出力
#22 で構築した screen_parallel_v2 に C×A 複合判定を追加し、1回の Runner 起動で「高配当 / C / A / C×A composite」4判定が自動で出る形に拡張します。9セルマトリクスは pandas pivot_table で集計します。
# screen_parallel_v3.py — 高配当 + C + A + C×A 複合 を1パイプラインで自動判定
# 動作環境: Python 3.11+ / duckdb 1.0+ / pandas 2.x
import duckdb
import pandas as pd
from multiprocessing import Pool, cpu_count
from datetime import date
from pathlib import Path
from spec_sheet_judge_v2 import judge_v2
from can_slim_c_judge import judge_c_element
from can_slim_a_judge import judge_a_element
from can_slim_composite_judge import judge_composite
DB_PATH = Path("data/stocks.duckdb")
OUT_DIR = Path("data/screening_results")
_RULES_BY_PROFILE: dict = {}
_C_THRESHOLD: float = 25.0
_C_MIN_CONSECUTIVE: int = 2
_A_THRESHOLD: float = 25.0
_A_MIN_CONSECUTIVE_YEARS: int = 3
_COMPOSITE_STRATEGY: str = "and"
def _init_worker(rules: dict, c_t: float, c_min: int,
a_t: float, a_min: int, comp_strat: str) -> None:
"""initializer パターン(応用編 #15 で確立、spawn 環境対応)"""
global _RULES_BY_PROFILE, _C_THRESHOLD, _C_MIN_CONSECUTIVE
global _A_THRESHOLD, _A_MIN_CONSECUTIVE_YEARS, _COMPOSITE_STRATEGY
_RULES_BY_PROFILE = rules
_C_THRESHOLD, _C_MIN_CONSECUTIVE = c_t, c_min
_A_THRESHOLD, _A_MIN_CONSECUTIVE_YEARS = a_t, a_min
_COMPOSITE_STRATEGY = comp_strat
def judge_one_ticker(metrics: dict) -> dict:
"""1銘柄に対して 高配当 + C + A + C×A の4判定を自動実施"""
profile = metrics.get("industry_profile") or "manufacturing"
rules = _RULES_BY_PROFILE.get(profile, _RULES_BY_PROFILE.get("manufacturing", {}))
high_div = judge_v2(metrics, rules)
c_v = judge_c_element(
{"yoy_growth_pct": metrics.get("yoy_growth_pct"),
"consecutive_up_quarters": metrics.get("consecutive_up_quarters")},
threshold_pass=_C_THRESHOLD, min_consecutive=_C_MIN_CONSECUTIVE,
)
a_v = judge_a_element(
{"cagr_3y_pct": metrics.get("cagr_3y_pct"),
"cagr_5y_pct": metrics.get("cagr_5y_pct"),
"consecutive_up_years": metrics.get("consecutive_up_years")},
threshold_pass=_A_THRESHOLD, min_consecutive_years=_A_MIN_CONSECUTIVE_YEARS,
)
composite = judge_composite(
{"c_verdict": c_v, "a_verdict": a_v}, strategy=_COMPOSITE_STRATEGY,
)
return {
"ticker_normalized": metrics["ticker_normalized"],
"name_jp": metrics.get("name_jp"),
"industry_profile": profile,
"high_dividend_overall": high_div["overall"],
"can_slim_c": c_v,
"can_slim_a": a_v,
"can_slim_composite": composite,
}
def aggregate_9cell_matrix(result_df: pd.DataFrame) -> pd.DataFrame:
"""C verdict × A verdict の 9セルマトリクスを集計(突発より連続性の俯瞰)"""
pivot = result_df.pivot_table(
index="can_slim_c", columns="can_slim_a",
values="ticker_normalized", aggfunc="count", fill_value=0,
)
pivot = pivot.reindex(index=["PASS", "CAUTION", "FAIL"],
columns=["PASS", "CAUTION", "FAIL"], fill_value=0)
return pivot
def run_screening_v3(composite_strategy: str = "and") -> Path:
"""高配当 + C + A + C×A 複合の自動判定パイプライン(v3)"""
OUT_DIR.mkdir(parents=True, exist_ok=True)
today = date.today().isoformat()
out_path = OUT_DIR / f"{today}_v3.parquet"
with duckdb.connect(str(DB_PATH)) as conn:
rules: dict = {}
rows = conn.execute("""
SELECT industry_profile, standard_indicator, direction,
threshold_pass, threshold_caution
FROM industry_indicator_map
""").fetchall()
for prof, ind, dirn, p, c in rows:
rules.setdefault(prof, {})[ind] = {
"direction": dirn, "threshold_pass": p, "threshold_caution": c,
"range_high_pass": None, "range_high_caution": None,
}
# 直近期 C 指標 と 直近年 A 指標を ROW_NUMBER OVER で取得
# H4 補足: yoy_growth_pct_capped を yoy_growth_pct として渡す = 赤字→黒字転換は NaN → CAUTION 扱い
# (ターンアラウンド銘柄は CAN-SLIM C で機械的に CAUTION 化される設計、原典準拠の制約)
df = conn.execute("""
SELECT s.*,
c.yoy_growth_pct, c.consecutive_up_quarters,
a.cagr_3y_pct, a.cagr_5y_pct, a.consecutive_up_years
FROM v_screening_input s
LEFT JOIN (
SELECT ticker_normalized,
yoy_growth_pct_capped AS yoy_growth_pct, -- 前年正値時のみ有効
consecutive_up_quarters
FROM (SELECT *, ROW_NUMBER() OVER
(PARTITION BY ticker_normalized
ORDER BY period_end DESC) AS rn
FROM quarterly_eps_with_yoy) ranked
WHERE rn = 1
) c ON s.ticker_normalized = c.ticker_normalized
LEFT JOIN (
SELECT ticker_normalized, cagr_3y_pct, cagr_5y_pct, consecutive_up_years
FROM (SELECT *, ROW_NUMBER() OVER
(PARTITION BY ticker_normalized
ORDER BY fiscal_year DESC) AS rn
FROM annual_eps_with_cagr) ranked
WHERE rn = 1
) a ON s.ticker_normalized = a.ticker_normalized
""").fetchdf()
metrics_list = df.to_dict(orient="records")
with Pool(processes=cpu_count(), initializer=_init_worker,
initargs=(rules, _C_THRESHOLD, _C_MIN_CONSECUTIVE,
_A_THRESHOLD, _A_MIN_CONSECUTIVE_YEARS, composite_strategy)) as pool:
results = pool.map(judge_one_ticker, metrics_list, chunksize=50)
result_df = pd.DataFrame(results)
result_df.to_parquet(out_path, index=False)
matrix = aggregate_9cell_matrix(result_df)
print(f"saved: {out_path}, rows={len(result_df)}")
print(f"\n9セルマトリクス(C verdict × A verdict):\n{matrix}")
print(f"\n複合 PASS: {(result_df['can_slim_composite']=='PASS').sum()}")
return out_path
if __name__ == "__main__":
run_screening_v3(composite_strategy="and")
9セルマトリクスの罠:左上集中が示すもの
- PASS×PASS が極端に多い: 閾値が緩すぎる可能性(特に新興市場相場)。閾値再調整を検討(ただしバックテスト過剰適合に注意)
- PASS×PASS が0件: 閾値が厳しすぎる、または市場全体が低成長期。市場の M 要素(CAN-SLIM の M)が弱気局面の可能性
- PASS×CAUTION(直近強気+長期不明)が多い: 新規上場ラッシュ期の特徴、上場初期の急成長銘柄。期待値低めで観察
- FAIL×PASS(直近失速+長期実績)が多い: 成熟期入り銘柄群、グロースから配当への移行候補
スニペット3:matplotlib ヒートマップと業種分散(HHI)の自動判定可視化
9セルマトリクスを matplotlib ヒートマップで可視化し、さらに複合 PASS 銘柄の業種分散(HHI、応用編 #19 で導入)を計算して集中リスクを定量化します。「絶対値より変化率、突発より連続性」原則の運用結果を視覚で確認できる設計です。※ 出力例の実画像は本記事に未掲載(読者環境で生成)。
# plot_composite_matrix.py — 9セルマトリクス + 業種分散の可視化
# 動作環境: Python 3.11+ / matplotlib 3.9+ / pandas 2.x / numpy
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from pathlib import Path
# 日本語フォント設定(環境横断対応)
matplotlib.rcParams["font.family"] = ["Hiragino Sans", "Yu Gothic", "Meiryo", "Noto Sans CJK JP", "DejaVu Sans"]
matplotlib.rcParams["axes.unicode_minus"] = False
def compute_hhi(industry_counts: pd.Series) -> float:
"""業種分散の HHI(Herfindahl-Hirschman Index、応用編 #19 で導入)
HHI = Σ(各業種シェア%)^2、最大10000、目安 2000 以上で集中、3000 以上で警告
"""
total = industry_counts.sum()
if total == 0:
return 0.0
shares = (industry_counts / total) * 100
return float((shares ** 2).sum())
def plot_composite_matrix(matrix: pd.DataFrame, hhi: float,
out_path: Path = Path("data/plots/composite_9cell.png")) -> Path:
"""9セルマトリクス(ヒートマップ)+ HHI の自動判定可視化
H2 修正: max=0(PASS銘柄ゼロ)の場合の白黒反転判定をガードし、全セル黒固定にしない
"""
out_path.parent.mkdir(parents=True, exist_ok=True)
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(matrix.values, cmap="Purples", aspect="auto")
ax.set_xticks(range(len(matrix.columns)))
ax.set_yticks(range(len(matrix.index)))
ax.set_xticklabels([f"A: {v}" for v in matrix.columns])
ax.set_yticklabels([f"C: {v}" for v in matrix.index])
ax.set_xlabel("A 要素 verdict(長期持続)")
ax.set_ylabel("C 要素 verdict(直近加速)")
# 各セルに件数を表示(H2 修正: max=0 のガード)
matrix_max = matrix.values.max()
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
count = matrix.iat[i, j]
if matrix_max == 0:
color = "black" # 全 0 のとき黒固定(読みにくさを回避するなら他のフォールバックも可)
else:
color = "white" if count > matrix_max * 0.5 else "black"
ax.text(j, i, str(count), ha="center", va="center", color=color, fontsize=14)
hhi_label = "OK" if hhi < 2000 else ("注意" if hhi < 3000 else "警告")
ax.set_title(f"CAN-SLIM C×A 9セルマトリクス(業種HHI={hhi:.0f}, {hhi_label})")
fig.colorbar(im, ax=ax, label="銘柄数")
fig.tight_layout()
fig.savefig(out_path, dpi=120)
plt.close(fig)
return out_path
def analyze_composite_pass(result_df: pd.DataFrame) -> dict:
"""複合 PASS 銘柄の業種分布と HHI を分析(突発より連続性の事後検証)"""
pass_df = result_df[result_df["can_slim_composite"] == "PASS"]
if pass_df.empty:
return {"pass_count": 0, "hhi": 0.0, "industry_dist": {}}
industry_counts = pass_df["industry_profile"].value_counts()
hhi = compute_hhi(industry_counts)
return {
"pass_count": int(len(pass_df)),
"hhi": hhi,
"hhi_status": "OK" if hhi < 2000 else ("注意" if hhi < 3000 else "警告"),
"industry_dist": industry_counts.to_dict(),
}
エンジニア的に言い換えると(生産技術の3軸 AND 検査ライン)
本コードは、製造業の生産技術で言う 「3軸 AND 検査ライン + 結果分布の可視化」に近い枠組みです。9セルマトリクスは「品質 verdict × 性能 verdict」の組合せ分布、HHI は「PASS 製品が特定の製造ラインに集中していないか」の集中度チェック。「絶対値より変化率、突発より連続性」原則を多軸 AND で運用した結果、業種が偏らないかを HHI で事後検証する2段構えです。
スニペット4:C×A PASS 銘柄の簡易バックテスト(概念実装、株価実値非掲載)
過去5年で C×A 両 PASS だった銘柄のその後12ヶ月の株価リターンを集計し、複合判定の有効性を簡易検証します。本記事は概念実装のみで実値は掲載しません(J-Quants/FMP 利用規約に基づく方針)。読者が自分の環境で再現する手順を示します。v2 H1/H3 修正: SQL ハードコード閾値を判定器呼び出しに置換、5y 優先 + 3y フォールバックを再現。
# backtest_composite.py — C×A PASS 銘柄の簡易バックテスト(v2: 判定器呼び出しに統一)
# 動作環境: Python 3.11+ / pandas 2.x / duckdb 1.0+
import duckdb
import pandas as pd
from pathlib import Path
from datetime import date, timedelta
from can_slim_c_judge import judge_c_element
from can_slim_a_judge import judge_a_element
from can_slim_composite_judge import judge_composite
DB_PATH = Path("data/stocks.duckdb")
def historical_composite_pass(as_of_date: date,
c_threshold: float = 25.0, c_min_consec: int = 2,
a_threshold: float = 25.0, a_min_years: int = 3,
strategy: str = "and") -> pd.DataFrame:
"""指定日時点の C×A 両 PASS 銘柄リストを判定器経由で再構築(v2: H1/H3 修正)
SQL は時系列フィルタ + 直近期データ抽出のみ、判定は judge_*_element に委譲
→ #23 の 5y 優先 + 3y フォールバックや #22 の3段階判定が再現される
"""
with duckdb.connect(str(DB_PATH)) as conn:
df = conn.execute(f"""
WITH c_at AS (
SELECT ticker_normalized,
yoy_growth_pct_capped AS yoy_growth_pct,
consecutive_up_quarters
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY ticker_normalized
ORDER BY period_end DESC) AS rn
FROM quarterly_eps_with_yoy
WHERE period_end <= DATE '{as_of_date.isoformat()}'
) ranked WHERE rn = 1
),
a_at AS (
SELECT ticker_normalized, cagr_3y_pct, cagr_5y_pct, consecutive_up_years
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY ticker_normalized
ORDER BY fiscal_year DESC) AS rn
FROM annual_eps_with_cagr
WHERE period_end <= DATE '{as_of_date.isoformat()}'
) ranked WHERE rn = 1
)
SELECT c.ticker_normalized,
c.yoy_growth_pct, c.consecutive_up_quarters,
a.cagr_3y_pct, a.cagr_5y_pct, a.consecutive_up_years
FROM c_at c
JOIN a_at a ON c.ticker_normalized = a.ticker_normalized
""").fetchdf()
rows = []
for _, r in df.iterrows():
c_v = judge_c_element(
{"yoy_growth_pct": r.get("yoy_growth_pct"),
"consecutive_up_quarters": r.get("consecutive_up_quarters")},
threshold_pass=c_threshold, min_consecutive=c_min_consec,
)
a_v = judge_a_element(
{"cagr_3y_pct": r.get("cagr_3y_pct"),
"cagr_5y_pct": r.get("cagr_5y_pct"),
"consecutive_up_years": r.get("consecutive_up_years")},
threshold_pass=a_threshold, min_consecutive_years=a_min_years,
)
comp = judge_composite({"c_verdict": c_v, "a_verdict": a_v}, strategy=strategy)
if comp == "PASS":
rows.append({"ticker_normalized": r["ticker_normalized"],
"c_verdict": c_v, "a_verdict": a_v})
return pd.DataFrame(rows)
def compute_forward_return(ticker: str, start_date: date,
months: int = 12) -> float | None:
"""指定銘柄の start_date から months ヶ月後の株価リターン(実装は読者環境で)
本記事は概念実装。実際の取得は:
- 日本株: J-Quants /prices/daily_quotes
- 米国株: FMP /historical-price-full/{symbol}
で start_date と start_date + months の終値を取得 → リターン計算
"""
# 概念実装のため None を返す
# end_date = start_date + timedelta(days=months * 30)
# start_price = fetch_close_price(ticker, start_date)
# end_price = fetch_close_price(ticker, end_date)
# return (end_price / start_price - 1.0) * 100
return None
def backtest_summary(start_dates: list[date]) -> pd.DataFrame:
"""各 as_of_date 時点で C×A PASS だった銘柄の12ヶ月後リターンを集計"""
rows = []
for d in start_dates:
pass_df = historical_composite_pass(d)
for ticker in pass_df["ticker_normalized"]:
ret = compute_forward_return(ticker, d, months=12)
rows.append({"as_of_date": d, "ticker": ticker, "forward_return_pct": ret})
return pd.DataFrame(rows)
def report_backtest(bt_df: pd.DataFrame) -> dict:
"""バックテスト結果のサマリ(勝率 / 平均リターン / 中央値)"""
valid = bt_df.dropna(subset=["forward_return_pct"])
if valid.empty:
return {"sample_size": 0}
return {
"sample_size": len(valid),
"win_rate_pct": float((valid["forward_return_pct"] > 0).mean() * 100),
"mean_return_pct": float(valid["forward_return_pct"].mean()),
"median_return_pct": float(valid["forward_return_pct"].median()),
"max_drawdown_pct": float(valid["forward_return_pct"].min()),
}
バックテストの罠:生存バイアスとデータスヌーピング
- 生存バイアス: 過去 PASS 銘柄リストを「現在も存在する銘柄」だけで作ると、上場廃止された失敗銘柄が抜けて勝率が過大評価される。J-Quants の上場廃止銘柄含むマスタを活用
- データスヌーピング: 同じ過去データで閾値を最適化すると過剰適合。閾値は理論ベース(CAN-SLIM 原典)で固定し、検証期間を分けるのが基本
- 勝率の解釈: バックテスト勝率は 50-55% が標準、65% を超える結果は過剰適合の疑い。原典準拠 + 期間分離の場合の実測値感は概ね 55-60% 程度
- サンプル数: 米国 1-3% × 数千銘柄 × 5年 = 数百サンプル程度。日本市場は更に少なく、統計的有意性に注意
- 本記事の方針: 概念実装のみ提示、勝率値は掲載せず。読者環境での再現を促す
設計判断の記録:C×A 複合判定の3判断 + 応用編〜発展編 #24 の俯瞰表(33件)
判断1:複合戦略のデフォルトを AND / OR / 重み付けのどれにするか
- 採用理由: AND 戦略をデフォルト。代替案との比較:
- OR 戦略: 抽出数が AND の3-5倍に増えるが、偽陽性も同程度増加。教育目的・候補リスト用
- 重み付け戦略: 連続値で柔軟だが、重み(c_weight=0.6 等)と閾値の調整に恣意性が混入
- 採用:AND 戦略: CAN-SLIM 原典の「両軸を同時に満たす」思想に最も忠実、偽陽性低
- 採用したことで失うもの: 抽出数(米国 1-3%、日本 0.5-1%)が少なく、市場全体の流れを見にくい
- トリガー条件: 候補リストを広く取りたい →
strategy="or"。スコア化したい →strategy="weighted" - 残るメリット: 偽陽性最小化、運用判断のシンプルさ、原典思想への忠実さ
- 製造業の評価作法: 「品質 × 性能 × コスト」の3軸 AND 判定で量産投資を決める作法と同じ枠組み
判断2:9セルマトリクスを基本表示にするか PASS×PASS 件数のみにするか
- 採用理由: 9セル全表示をデフォルト。PASS×PASS 件数だけだと「閾値が厳しすぎる/緩すぎる」の判断材料がない
- 採用したことで失うもの: 表示の単純さ(投資家向けレポートでは PASS×PASS のみのほうが訴求力高い場合あり)
- トリガー条件: 投資家向けレポート → PASS×PASS のみハイライト + 補足として9セル添付の2段表示
- 残るメリット: 閾値調整の判断材料、9セル分布の経時変化が市場状況のシグナルになる
- 品質管理での運用: 「全 verdict の組合せ分布」を可視化するのが PDCA の標準作法
判断3:バックテストを記事に実装するか概念実装に留めるか
- 採用理由: 概念実装のみ(株価データ取得・実値計算は読者環境で)。J-Quants/FMP 利用規約遵守の方針継承
- 採用したことで失うもの: 「勝率 60%」のような訴求力の高い数値表現の機会
- トリガー条件: 自分の環境でデータ取得できる読者は実装可能、関数シグネチャと処理フローは公開
- 残るメリット: 利用規約遵守、生存バイアス・データスヌーピングへの注意喚起ができる
- 研究現場の作法: 「再現可能な手順」と「実値」を分けるのが研究データ公開のセオリー
応用編 #13〜#21 + 発展編 #22〜#24 の主要設計判断 俯瞰表(33件)
応用編 + 発展編全件を本記事で全展開します(v2 D2 修正、累計33件):
| 記事 | 主要判断 | 採用 |
|---|---|---|
| #13 | データソース選定 | J-Quants + EDINET(公式 API 優先) |
| #14 | DB / アーキテクチャ | DuckDB + ELT + マスタ駆動 |
| #14 | 業種別補正設計 | industry_indicator_map + direction |
| #14 | 正規化キー | ticker_normalized 5桁0埋め |
| #15 | 並列化技術 | multiprocessing(initializer パターン) |
| #15 | 通知メディア | LINE Messaging API |
| #15 | スケジューラ | cron → GitHub Actions |
| #16 | 連続増配判定 | 連続非減少(同額据え置きOK) |
| #16 | EPS 安定性指標 | 変動係数 CV + 線形回帰 |
| #16 | 新規上場銘柄の扱い | 5期未満は CAUTION 強制 |
| #17 | 異常値検知の組合せ | IQR + Z + SPC + WER の3-4層フィルタ |
| #17 | 市場全体ショック対応 | 絶対値 + #20 で TOPIX 相対値併用 |
| #18 | 業種粒度 | 10業種 → #20 で25業種 |
| #18 | 業種別主指標数 | 1業種1主指標 |
| #18 | マスタ更新サイクル | 四半期レビュー |
| #18 | テーブル PK 設計 | 複合PK / 単列PK の使い分け |
| #19 | 集中度指標 | HHI(投資視点閾値 2000/3000) |
| #19 | 相関分析期間 | 過去24ヶ月(月次) + 直近6ヶ月併用 |
| #19 | FMEA RPN 重み | 等倍積(S × O × D) |
| #19 | ポートフォリオテーブル | 個別銘柄評価とは別テーブル |
| #20 | master_runner 方式 | NotImplementedError による委譲 |
| #20 | 業種カバレッジ | 25業種で東証時価総額の概ね9割 |
| #21 | 成長株フレームワーク | CAN-SLIM |
| #21 | パイプライン再利用 | 応用編流用、判定層のみ拡張 |
| #21 | 市場カバレッジ | 日米両市場(FMP + SEC EDGAR) |
| #22 | YoY 計算ロジック | 絶対値分母 |
| #22 | 連続性判定 | min_consecutive=2 |
| #23 | CAGR 期間優先順位 | 5年優先 + 3年フォールバック |
| #23 | 赤字期 CAGR 扱い | None で返し CAUTION |
| #23 | 可視化粒度 | PASS 銘柄のみ詳細可視化 |
| #24 | 複合戦略デフォルト | AND(OR/重み付け 切替可) |
| #24 | 9セル表示粒度 | 9セル全表示(投資家レポート時のみ PASS×PASS ハイライト) |
| #24 | バックテスト粒度 | 概念実装のみ(実値非掲載) |
本業の話:品質×性能×コストの3軸 AND 判定で量産化成功率を上げた経験
筆者が研究開発部門で新製品の量産化判定に関わっていた初期、単軸の高得点で量産投資を決めて失敗した経験があります。ベテランから次の指導を受けてから3軸 AND 判定に切替えました:
- 「1軸が PASS でも、他の軸が CAUTION なら量産するな。性能だけ突出していても、品質 PASS かつコスト PASS の3軸 AND が原則」
- 「verdict 全体の分布を見ろ。3×3×3 = 27セルマトリクスで分布を見て、3軸 PASS が極端に少ないなら閾値が厳しすぎ、多すぎるなら緩すぎ」
- 「PDCA の事後検証で偽陽性を測れ。3軸 AND PASS で量産化した製品の市場ライフサイクル vs 単軸 PASS だった製品の比較で、AND 判定の有効性を定量化」
具体的な業務インパクトと3-4年スパンのマイルストーン:
- 1年目(失敗): 性能単軸で量産化決定したプロトタイプが、量産後に品質トラブル続発。市場投入後3ヶ月で改修プロジェクト発生(特定製品ラインで影響、サンプル 約3件)
- 2年目(指摘 + 改修): ベテラン指摘で3軸 AND 判定に切替。過去データを再評価し、当時 AND PASS だった製品のみで再シミュレーション。偽陽性率が約60%減(測定方法: 量産後12ヶ月以内に改修プロジェクトに発展した製品の比率を、新基準と旧基準で比較。サンプル 過去3年分の量産化案件 約30件 のシミュレーション再評価)
- 3年目(運用): 3軸 AND 判定 + 27セルマトリクス可視化が量産化会議の標準資料に。「絶対値より変化率、突発より連続性」原則の多軸版が部内浸透
- 4年目(成果): 量産化された製品の市場投入後12ヶ月の改修プロジェクト発生率が前年比約3割減(測定方法: 改修プロジェクト件数 ÷ 量産化された製品数。サンプル 新基準採用後2年間の量産化案件 約12件)
本記事の C×A 複合判定(特に AND 戦略)は、この本業の3軸 AND 判定経験と重なります。CAN-SLIM 原典の「両軸 PASS で成長株候補の最有力ゾーン」思想が、製造業の品質管理での「3軸 AND PASS で量産候補」思想と本質的に共通しているのが、本記事の核心的な発見です。
逆方向の転移:投資の C×A マトリクス可視化が本業の3軸検査ラインを強化
本記事の9セルマトリクス(pivot_table + matplotlib ヒートマップ)の実装パターンを、本業の27セル3軸マトリクス可視化に逆輸入する余地があります。本業ではこれまで27セルを表形式で見ていましたが、ヒートマップ化することで「閾値調整時のセル分布の変化」を直感的に追えるようになる見込みです。応用編で確立した双方向の知識循環が発展編でも続いています。
まとめ:C×A 複合判定で成長株候補の最有力ゾーンを抽出、CAN-SLIM 実装ブロック完結
- CAN-SLIM C×A 複合判定は「直近加速(C)+ 長期持続(A)両軸 PASS」を AND 戦略で機械的に自動判定。3戦略(AND/OR/重み付け)から運用に応じて選択、9セルマトリクスで verdict 分布を可視化、業種分散(HHI)と組合せて偽陽性を削減。多軸 AND 判定への原則拡張で、本記事は判定層の集大成として機能する
- screen_parallel_v3 への統合で、1 Runner 起動で「高配当 / C / A / C×A 複合」4判定が自動で出る形に拡張完了。応用編で確立した判定器パターン・initializer パターン・DuckDB トランザクション境界・「絶対値より変化率、突発より連続性」原則を全継承
- 発展編 CAN-SLIM 実装ブロック完結(#22→#23→#24): (1) #22 で C 単独実装、(2) #23 で A 単独実装、(3) 本記事 #24 で C×A 複合判定 + 9セル可視化 + 業種分散 + バックテスト概念。次回 #25 以降は事業構造分析(製品アーキテクト視点)に進む
- 機械 PASS は「成長株候補の入口」、購入候補ではない: CAN-SLIM 7要素中2要素のみの充足であり、ピーク EPS 型失速(Zoom 2021・Peloton 2022・半導体ピーク 2021Q4 型)など複合 PASS でも残るリスクあり。本記事冒頭のポートフォリオ運用ルール(5-8銘柄、損切 -8%、利確 +20-25%)を併用してください
今日からできる3つのアクション
- #22-#23 を未実装の方はそちらから先に。本記事のスニペットは
quarterly_eps_with_yoyとannual_eps_with_cagrテーブルが揃っている前提です。実装済みの方はスニペット1〜2でscreen_parallel_v3を構築し、自分の環境で「高配当 / C / A / C×A」4判定 + 9セルマトリクスを出力。米国 S&P500 で C×A AND PASS は概ね 1-3%、東証プライムで 0.5-1% 程度を想定して期待値設定 - スニペット3 で9セルマトリクスのヒートマップ + HHIを計算し、複合 PASS 銘柄が業種集中していないか確認。HHI が3000を超えたら IT・半導体偏重の警告が出る設計。冒頭の「ポートフォリオ運用ルール」と合わせて、保有銘柄数 5-8 + 1業種上限 30% などのルール設定を検討
- スニペット4 のバックテスト概念実装を参考に、過去5年で C×A PASS だった銘柄のその後12ヶ月リターンを自分の環境で集計。生存バイアス・データスヌーピングに注意しつつ、勝率(標準値 50-55%)や平均リターンの感触を掴む
次回予告:CAN-SLIM の N(製品・サービス革新)— 製品アーキテクト視点の事業構造分析
次回(記事#25)からは、定量分析だけでは捉えきれない「事業構造の質」を製品アーキテクト視点で分析する手法に進みます。本記事で構築した C×A 複合判定が「網を張る」フェーズなら、#25 以降は「網にかかった候補から本当に投資すべきものを選ぶ」フェーズです。発展編後半のロードマップ:
- #25 製品アーキテクト視点: CAN-SLIM の N(New Product/Service)を製品開発DXの目線で評価する手法
- #26 PCA で銘柄相関分析: 機械学習の主成分分析で C×A PASS 銘柄群の構造を理解
- #27 情報エントロピーで業種分散: 応用編 #19 HHI を情報理論で再解釈
- #28 NLP で N 要素の自然言語判定: 決算説明資料・有報の文章から「製品革新」シグナルを抽出
「製品開発DXエンジニアの投資術」シリーズ全体像
本記事は 発展編(記事#21〜#30)の第4回 — CAN-SLIM 実装ブロック完結回 です。
- 基礎編(#01〜#10): 完了
- 応用編(#11〜#20): 完了 ✅
- 発展編(#21〜#30): 進行中
発展編 ロードマップ:
- 導入:#21 なぜ成長株か
- CAN-SLIM 実装:#22 C 当期EPS → #23 A 年間EPS → ▶ イマココ #24 C×A 複合(本記事)
- 事業構造:#25 製品アーキテクト視点(次回、「網から選ぶ」フェーズ)
- 機械学習・統計:#26 PCA → #27 エントロピー → #28 NLP(N要素)
- ハイブリッド + 総括:#29 配当 × 成長 戦略 → #30 シリーズ総括
▶ 前回 #23 CAN-SLIM A | 本記事 #24 C×A 複合判定 | 次回 #25(製品アーキテクト視点、公開予定)
関連記事(応用編から): #15 全銘柄スクリーニング自動化 | #16 配当推移の安定性 | #19 業種分散 FMEA | #20 応用編まとめ
免責事項(再掲)
本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。CAN-SLIM C×A 複合判定の閾値(C: YoY +25%、A: 5年 CAGR +25% + 連続増益3年)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants API・FMP の利用規約は変更される可能性があるため、実装時は各サービスの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants/FMP から取得した実データ・株価実値は掲載していません。バックテスト結果は概念実装のみで、生存バイアス・データスヌーピング等の罠に必ずご注意ください。記事中で言及した特定企業(Zoom Video / Peloton 等)の株価事例は CAN-SLIM の限界を示す教育的引用であり、特定銘柄の売買推奨ではありません。

コメント