ハイブリッド戦略|配当 × 成長 を Python 自動判定でポートフォリオ統合

Chelsea-Labs #29 サムネイル

免責事項

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。ハイブリッド戦略のミックス比率・ライフステージ別の配分・NISA / iDeCo の口座別最適化ロジックは教育目的の参考値であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。本記事中に J-Quants API・FMP から取得した個別銘柄の実値・株価実値は掲載していません(教育目的の概念例に限定、利用規約に基づく方針)。CAN-SLIM はオニール(William J. O’Neil)著「How to Make Money in Stocks」で提唱された手法を基にエンジニア視点で再構築しています。

応用編 #11-#20 で 高配当株のスクリーニングパイプラインを、発展編 #22-#28 で CAN-SLIM 成長株の自動判定基盤(C/A/N + PCA + 情報エントロピー + NLP)を構築してきました。本記事 #29 は、両者を統合する ハイブリッド戦略を Python で自動判定する手順とサンプルコード入門です。応用編 + 発展編の全要素を集約し、5-8銘柄の最終ポートフォリオを構築 + NISA / iDeCo 口座別最適化 + 月次リバランスの LINE 通知まで自動化します。

「配当のディフェンシブ性」と「成長の高リターン性」は、投資戦略として補完的です。高配当株は市場下落局面でのクッション、成長株は強気相場での超過リターン。両者を組合わせるハイブリッド戦略は、ライフステージや市場環境に応じてミックス比率を調整する柔軟性が強みです。「絶対値より変化率、突発より連続性」原則を、ポートフォリオの長期運用に拡張するのが本記事の核心です。

多くの読者がぶつかる壁:

  • 統合スコアの設計: 高配当 PASS と CAN-SLIM C×A×N PASS をどう数値化して統合するか(重み付け平均 / 階層判定 / can_slim_composite 優先利用)
  • ライフステージ別ミックス比率: 20-30代は成長70% + 配当30%、60代+は逆転、という参考値の根拠と運用
  • 事業構造分析の活用: #26 PCA で銘柄相関、#27 情報エントロピーで業種分散、#28 NLP で意味的フィルタの3軸を最終選択にどう組合わせるか
  • 5-8銘柄への絞り込み: 数百のPASS候補から #24 ポートフォリオ運用ルール準拠の少数精鋭を選ぶアルゴリズム(v2: 段階的緩和で透明性確保)
  • NISA / iDeCo 口座別最適化: 成長は新NISA、配当は配当控除を活かす課税口座、現金代替は iDeCo 等の振り分けロジック(v2 I2 追加)

筆者は製造業の研究開発部門で、マルチ製品ライン戦略を運用してきました。「高利益=低数量の主力製品 + 薄利多売の安定収益製品」の組合せでポートフォリオリスクを分散する作法は、配当 × 成長のハイブリッド戦略と等価の構造です。本業の経験を投資データに転用する典型例として、本記事のエピソードで詳しく整理します。

本記事で扱う専門用語の予習

  • ハイブリッド戦略: 配当(インカム)と成長(キャピタルゲイン)を1つのポートフォリオで組合わせる戦略。両者の補完性でリスクを分散
  • ハイブリッドスコア: 高配当判定と CAN-SLIM 判定の重み付け統合スコア。本記事では 0〜10 の範囲で計算、can_slim_composite 優先モードもあり
  • ライフステージ別ミックス比率: 投資家の年齢・リスク許容度に応じた配当 vs 成長の配分目安。本記事は教育目的の参考値(投資助言ではない)
  • 5層フィルタチェーン: 高配当 PASS → CAN-SLIM PASS → PCA 多様性 → エントロピー分散 → NLP 意味的フィルタ → 5-8銘柄選択 の5層階層フィルタ
  • 段階的緩和(relaxation_stage、v2 T2 追加): フォールバック時に Layer 4 → Layer 3 → Layer 2 の順で1段ずつ緩和、サイレント全捨ての回避
  • リバランス: ポートフォリオの構成比率を定期的に目標値に戻す自動判定運用。月次・四半期・年次で実施
  • NISA / iDeCo 口座別最適化: 新NISA成長枠/つみたて枠/iDeCo/課税口座の4口座に銘柄を税制効果最大化の観点で振り分け

「実装したくない読者」向け代替案

「ハイブリッド戦略を自分で実装するのは大変」という読者には、マネックス証券のロボアド「ON COMPASS」SBI証券のラップ口座米国 Wealthfront / Betterment のロボアドで類似機能が利用可能です。本記事の自動判定は「自分の戦略軸で日米両市場のハイブリッド戦略を運用したい」用途で、結果だけ見たい読者には不要です。

本記事の前提と難易度

  • 必須前提: 応用編 #15 高配当スクリーニング + 発展編 #22-#28 の全実装、または同等のパイプラインが動いている状態
  • numpy / pandas / scikit-learn / sentence-transformers の基本
  • 動作環境: Python 3.11+ / pandas 2.x / numpy / scikit-learn 1.4+ / requests 2.31+ / duckdb 1.0+
  • 戦略軸 → 戦術軸 → 事業構造軸 → 機械学習軸 → 情報理論軸 → NLP軸 → 統合軸: #21(戦略)→ #22-#28(戦術~NLP)→ 本記事 #29(統合) → #30(シリーズ総括)

ハイブリッド戦略 固有のリスク

  • ミックス比率の硬直化: 一度設定したライフステージ別比率に固執すると、市場環境変化に追従できない。年次でレビュー推奨
  • 統合判定の偽 PASS: 高配当 PASS かつ CAN-SLIM PASS でも、両者が異なる業種に偏れば結果的に分散投資にならない可能性
  • ライフステージ判定の罠: 「年齢 = リスク許容度」は単純化しすぎ、収入安定性・退職金準備状況・家族構成など複合要因を考慮すべき
  • 取引コストの累積: 月次リバランス × 5-8銘柄 × 売買手数料 で年間数千円〜数万円。SBI証券・楽天証券の手数料無料銘柄を活用
  • 税金の罠: 配当株からの配当金は20.315%課税、成長株の含み益は売却まで非課税。NISA 成長枠 + つみたて枠 + iDeCo + 課税口座の4口座戦略で実効税率を下げる(後段スニペット4で実装)

応用編 #16 安定性 + #19 HHI + #24 C×A + #25 N + #26 PCA + #27 エントロピー + #28 NLP + 本記事ハイブリッド戦略 + #24 ポートフォリオ運用ルールの全層統合が実装上の対策です。

本記事では 4個のサンプルコードで、統合判定 → ライフステージ別ミックス比率 → 事業構造分析活用の最終ポートフォリオ構築(5層フィルタ + 段階的緩和)→ LINE 通知連携(運用ルール反映 + NISA / iDeCo 口座別最適化)まで完成させます。応用編 + 発展編の全要素を統合する集約点。

結論:ハイブリッド戦略は配当 × 成長を統合し、ライフステージ別ミックス比率で運用する Python 自動判定の集約点。5層フィルタチェーン(高配当 → CAN-SLIM → PCA → エントロピー → NLP)+ 段階的緩和で 数百候補 → 5-8銘柄に絞り込み、#24 運用ルール(損切 -7〜-8% / 利確 +20-25% / 比率上限)を LINE 通知に反映、新NISA成長枠 / つみたて枠 / iDeCo / 課税口座の4口座最適化で税効率向上。応用編 #15 LINE 通知連携で月次リバランスを自動判定、次回 #30 シリーズ総括で30記事の学びを集約。

ライフステージ別ミックス比率の参考値(教育目的、投資助言ではない)

ライフステージ成長:配当 比率銘柄数目安リバランス頻度主な根拠
20-30代70 : 305-8銘柄月次長期投資期間でドローダウン耐性
40-50代50 : 506-10銘柄月次バランス重視、退職金準備期
60代+30 : 708-12銘柄四半期インカム重視、ドローダウン回避優先

これらは2026年5月時点の一般論的な参考値。実際の比率は収入安定性・家族構成・退職金準備状況・リスク許容度の自己評価で個別調整してください。

目次

ハイブリッド戦略の理論的背景:配当 × 成長 の補完性

配当株と成長株は市場局面で逆相関する傾向があります。理論的背景:

  • 強気相場: 成長株が市場をリード、配当株は相対的にアンダーパフォーム → 成長株の高リターンで全体を押し上げ
  • 弱気相場: 成長株は売り込まれる、配当株は配当による下値支持 → 配当株がポートフォリオのクッション
  • 横ばい相場: 配当のインカムが累積リターンに貢献 → 配当株が下支え
  • 長期: 両者の組合せはどちらか単独よりリスク調整後リターンが向上(ポートフォリオ理論の基本)

エンジニア的に言い換えると(マルチ製品ライン戦略)

製造業のマルチ製品ライン戦略で言う 「主力製品(高利益・少量)と安定製品(薄利・多量)の組合せ」同じ流儀です:

  • 主力製品(高利益・少量)= 成長株: 大きなリターンを狙うが、需要変動リスク高
  • 安定製品(薄利・多量)= 配当株: 高リターンは期待できないが、安定キャッシュフロー
  • マルチライン戦略の効果: 主力単独だと需要変動で売上が激しく上下、組合せでキャッシュフロー安定

絶対値より変化率、突発より連続性」原則をハイブリッド戦略に適用すると、ポートフォリオ全体のリターンの絶対値より、月次/四半期のリターン変動率を観察し、突発的な急変と継続的なシフトを区別する観察軸になります。

スニペット1:高配当 + CAN-SLIM PASS の統合自動判定でハイブリッドスコアを計算

応用編 #15 の screen_parallel と発展編の screen_parallel_v3 出力をマージして、銘柄ごとに 0〜10 のハイブリッドスコアを自動判定で計算します。v2 T3 修正: can_slim_composite が PASS なら優先利用、CAUTION/FAIL/None のときに c+a+n の単純平均にフォールバックする2系統設計

# hybrid_score.py — 高配当 + CAN-SLIM の統合自動判定でハイブリッドスコアを計算(v2 T3)
# 動作環境: Python 3.11+ / pandas 2.x / numpy
import pandas as pd
import numpy as np
from typing import Literal, TypedDict, NotRequired

Verdict = Literal["PASS", "CAUTION", "FAIL"]

_VERDICT_SCORE = {"PASS": 2, "CAUTION": 1, "FAIL": 0}

class HybridMetrics(TypedDict):
    high_dividend_overall: NotRequired[Verdict | None]
    can_slim_c: NotRequired[Verdict | None]
    can_slim_a: NotRequired[Verdict | None]
    can_slim_n: NotRequired[Verdict | None]
    can_slim_composite: NotRequired[Verdict | None]

def compute_hybrid_score(metrics: HybridMetrics,
                            growth_weight: float = 0.5,
                            dividend_weight: float = 0.5,
                            prefer_composite: bool = True) -> dict:
    """ハイブリッドスコアを 0〜10 で自動計算(v2 T3: composite 優先利用)

    prefer_composite=True (デフォルト): can_slim_composite (PASS/CAUTION/FAIL) を優先利用
        #24 C×A AND 設計の意図を尊重、C=PASS/A=FAIL/N=PASS が「中庸」になる罠を回避
    prefer_composite=False or composite が None: c+a+n の単純平均にフォールバック
    """
    total_weight = growth_weight + dividend_weight
    if total_weight > 0:
        gw = growth_weight / total_weight
        dw = dividend_weight / total_weight
    else:
        gw, dw = 0.5, 0.5

    div_verdict = metrics.get("high_dividend_overall") or "CAUTION"
    div_score = _VERDICT_SCORE.get(div_verdict, 1)

    # v2 T3: 成長スコアの算出を can_slim_composite 優先に分岐
    composite_verdict = metrics.get("can_slim_composite")
    if prefer_composite and composite_verdict in ("PASS", "CAUTION", "FAIL"):
        growth_score = _VERDICT_SCORE[composite_verdict]
        growth_method = "composite_preferred"
    else:
        c_score = _VERDICT_SCORE.get(metrics.get("can_slim_c") or "CAUTION", 1)
        a_score = _VERDICT_SCORE.get(metrics.get("can_slim_a") or "CAUTION", 1)
        n_score = _VERDICT_SCORE.get(metrics.get("can_slim_n") or "CAUTION", 1)
        growth_score = (c_score + a_score + n_score) / 3.0
        growth_method = "individual_average"

    hybrid_raw = (growth_score * gw + div_score * dw)  # 0-2
    hybrid_score = hybrid_raw * 5  # 0-10 にスケール

    return {
        "hybrid_score": float(hybrid_score),
        "growth_score": float(growth_score),
        "growth_method": growth_method,
        "dividend_score": float(div_score),
        "growth_weight": gw,
        "dividend_weight": dw,
    }

def judge_hybrid(metrics: HybridMetrics,
                  growth_weight: float = 0.5,
                  dividend_weight: float = 0.5,
                  pass_threshold: float = 7.0,
                  caution_threshold: float = 5.0,
                  prefer_composite: bool = True) -> Verdict:
    """ハイブリッドスコアから3段階の自動判定"""
    score_dict = compute_hybrid_score(
        metrics, growth_weight, dividend_weight, prefer_composite=prefer_composite
    )
    score = score_dict["hybrid_score"]
    if score >= pass_threshold:
        return "PASS"
    if score >= caution_threshold:
        return "CAUTION"
    return "FAIL"

ハイブリッドスコアの罠:重み付けの恣意性

  • 50:50 デフォルトの妥当性: ライフステージ別に変えるのが本来。本記事は次のスニペット2でライフステージ別自動切替を実装
  • 欠損 verdict の扱い: None や “CAUTION” として扱うが、本来は「データ未取得」を区別すべきケースも
  • composite 優先 vs 単純平均: v2 T3 修正で prefer_composite=True がデフォルト、#24 C×A AND 設計の意図を尊重。False にすると c+a+n の単純平均にフォールバック

スニペット2:ライフステージ別ミックス比率の自動判定モデル

投資家の年齢・リスク許容度に応じて、成長 vs 配当の重みを自動調整します。本記事は教育目的の参考値で、実運用は個別事情で調整してください。v2 T1 修正: 市場環境別動的調整後の clamp で重み合計が 1.0 から外れる罠を、再正規化で解消

# lifestage_mix.py — ライフステージ別の成長:配当 ミックス比率モデル(教育目的の参考値、v2 T1)
# 動作環境: Python 3.11+
from typing import Literal, TypedDict

LifeStage = Literal["20s_30s", "40s_50s", "60s_plus"]
RiskTolerance = Literal["aggressive", "moderate", "conservative"]

class MixWeights(TypedDict):
    growth_weight: float
    dividend_weight: float
    n_stocks_min: int
    n_stocks_max: int
    rebalance_frequency: str

_BASE_MIX_TABLE: dict[tuple[LifeStage, RiskTolerance], MixWeights] = {
    ("20s_30s", "aggressive"):   {"growth_weight": 0.8, "dividend_weight": 0.2, "n_stocks_min": 5, "n_stocks_max": 7, "rebalance_frequency": "monthly"},
    ("20s_30s", "moderate"):     {"growth_weight": 0.7, "dividend_weight": 0.3, "n_stocks_min": 5, "n_stocks_max": 8, "rebalance_frequency": "monthly"},
    ("20s_30s", "conservative"): {"growth_weight": 0.5, "dividend_weight": 0.5, "n_stocks_min": 6, "n_stocks_max": 9, "rebalance_frequency": "monthly"},
    ("40s_50s", "aggressive"):   {"growth_weight": 0.6, "dividend_weight": 0.4, "n_stocks_min": 6, "n_stocks_max": 9, "rebalance_frequency": "monthly"},
    ("40s_50s", "moderate"):     {"growth_weight": 0.5, "dividend_weight": 0.5, "n_stocks_min": 6, "n_stocks_max": 10, "rebalance_frequency": "monthly"},
    ("40s_50s", "conservative"): {"growth_weight": 0.3, "dividend_weight": 0.7, "n_stocks_min": 7, "n_stocks_max": 11, "rebalance_frequency": "quarterly"},
    ("60s_plus", "aggressive"):  {"growth_weight": 0.4, "dividend_weight": 0.6, "n_stocks_min": 7, "n_stocks_max": 11, "rebalance_frequency": "quarterly"},
    ("60s_plus", "moderate"):    {"growth_weight": 0.3, "dividend_weight": 0.7, "n_stocks_min": 8, "n_stocks_max": 12, "rebalance_frequency": "quarterly"},
    ("60s_plus", "conservative"): {"growth_weight": 0.2, "dividend_weight": 0.8, "n_stocks_min": 9, "n_stocks_max": 12, "rebalance_frequency": "quarterly"},
}

def get_mix_weights(life_stage: LifeStage,
                       risk_tolerance: RiskTolerance = "moderate") -> MixWeights:
    """ライフステージとリスク許容度から ミックス比率と銘柄数目安を取得"""
    return _BASE_MIX_TABLE.get(
        (life_stage, risk_tolerance),
        _BASE_MIX_TABLE[("40s_50s", "moderate")],
    )

def adjust_for_market_regime(base_weights: MixWeights,
                                market_regime: Literal["bull", "bear", "neutral"]) -> MixWeights:
    """市場環境(強気/弱気/中立)に応じた動的調整(v2 T1: clamp 後の再正規化追加)

    bull: 成長重みを +0.1 シフト
    bear: 配当重みを +0.1 シフト
    neutral: ベースのまま
    clamp で範囲 [0, 1] に丸めた後、合計 1.0 になるよう再正規化(不変条件維持)
    """
    adjusted = dict(base_weights)
    if market_regime == "bull":
        adjusted["growth_weight"] = min(1.0, base_weights["growth_weight"] + 0.1)
        adjusted["dividend_weight"] = max(0.0, base_weights["dividend_weight"] - 0.1)
    elif market_regime == "bear":
        adjusted["growth_weight"] = max(0.0, base_weights["growth_weight"] - 0.1)
        adjusted["dividend_weight"] = min(1.0, base_weights["dividend_weight"] + 0.1)

    # v2 T1: clamp 後に合計が 1.0 から外れた場合は再正規化(不変条件維持)
    total = adjusted["growth_weight"] + adjusted["dividend_weight"]
    if total > 0 and abs(total - 1.0) > 1e-9:
        adjusted["growth_weight"] = adjusted["growth_weight"] / total
        adjusted["dividend_weight"] = adjusted["dividend_weight"] / total
    return adjusted  # type: ignore[return-value]

ライフステージ別比率の罠:個別事情の重要性

  • 年齢一辺倒の単純化: 「年齢 = リスク許容度」ではない。30代でも住宅ローン重い・家族扶養多い人は配当重視、60代でも年金充実・健康な人は成長重視の選択肢あり
  • 市場環境の動的調整: adjust_for_market_regime で強気/弱気の判定を組合わせる。市場判定は CAN-SLIM の M 要素(応用編 #20 TOPIX 相対値で代替可能)
  • NISA / iDeCo の活用: 後段スニペット4 の allocate_to_accounts で4口座振り分け実装
  • 本コードの方針: 教育目的の参考値、自分の事情に合わせて MixWeights を直接編集して運用

スニペット3:5層フィルタチェーンと段階的緩和で最終 5-8 銘柄を自動判定抽出

事業構造分析ブロック(#26 PCA + #27 情報エントロピー + #28 NLP)の出力を活用し、数百のハイブリッドスコア PASS 候補から 5-8銘柄に絞り込む5層フィルタチェーンを実装します。v2 T2 修正: フォールバック時のサイレント全捨てを廃止、relaxation_stage で段階的緩和を可視化

# five_layer_filter.py — 5層フィルタチェーン + 段階的緩和(v2 T2: relaxation_stage 可視化)
# 動作環境: Python 3.11+ / pandas 2.x / numpy
import pandas as pd
import numpy as np
from typing import Literal

def _apply_filter(df: pd.DataFrame, filter_set: set | None, key: str) -> pd.DataFrame:
    """ヘルパ: filter_set があれば key 列で絞り込み、なければそのまま返す"""
    if not filter_set:
        return df
    return df[df[key].isin(filter_set)]

def filter_chain_to_final_picks(screening_result: pd.DataFrame,
                                  hybrid_pass_tickers: set[str],
                                  pca_diversified_picks: set[str] | None = None,
                                  entropy_status: Literal["PASS", "CAUTION", "FAIL"] = "PASS",
                                  nlp_pass_tickers: set[str] | None = None,
                                  target_n_min: int = 5,
                                  target_n_max: int = 8) -> dict:
    """5層フィルタチェーン + 段階的緩和で最終ポートフォリオ候補を自動判定抽出(v2 T2)

    Layer 1: ハイブリッドスコア PASS(必須、緩和不可)
    Layer 2: PCA 多様性選択(緩和段階1で除外)
    Layer 3: 情報エントロピー PASS(警告フラグのみ、絞り込みはしない)
    Layer 4: NLP 意味的フィルタ(緩和段階2で除外)
    Layer 5: 5-8銘柄に絞り込み(ハイブリッドスコア降順)

    フォールバック: target_n_min 未満 → relaxation_stage を 0→1→2 と段階的に上げる
    各段階の絞込結果と最終 relaxation_stage は返却 dict に明示
    """
    initial_count = len(screening_result)
    layer_counts = {"initial": initial_count}

    # Layer 1: ハイブリッドスコア PASS(必須)
    df_l1 = screening_result[screening_result["ticker_normalized"].isin(hybrid_pass_tickers)]
    layer_counts["layer1_hybrid"] = len(df_l1)

    # Layer 3: エントロピー警告のみ(絞り込みはしない)
    layer3_warning = entropy_status == "FAIL"

    # 段階的緩和: relaxation_stage 0 → 1 → 2 で順次緩める
    for stage in (0, 1, 2):
        if stage == 0:
            # PCA + NLP の両方を適用
            df = _apply_filter(df_l1, pca_diversified_picks, "ticker_normalized")
            df = _apply_filter(df, nlp_pass_tickers, "ticker_normalized")
        elif stage == 1:
            # NLP は緩和(PCA のみ適用)
            df = _apply_filter(df_l1, pca_diversified_picks, "ticker_normalized")
        else:
            # PCA も緩和(ハイブリッドスコアのみ)
            df = df_l1

        df_sorted = df.sort_values("hybrid_score", ascending=False)
        final_picks = df_sorted.head(target_n_max)
        if len(final_picks) >= target_n_min:
            return {
                "initial_count": initial_count,
                "layer_counts": layer_counts,
                "layer3_warning": layer3_warning,
                "relaxation_stage": stage,    # v2 T2: 何段階緩和したかを明示
                "final_picks": final_picks["ticker_normalized"].tolist(),
                "final_count": len(final_picks),
                "final_df": final_picks,
            }

    # 全段階で target_n_min 未満なら、最後の段階の結果を返す(CAUTION 推奨)
    return {
        "initial_count": initial_count,
        "layer_counts": layer_counts,
        "layer3_warning": layer3_warning,
        "relaxation_stage": 3,     # 全段階緩和でも未達
        "final_picks": final_picks["ticker_normalized"].tolist(),
        "final_count": len(final_picks),
        "final_df": final_picks,
        "warning": "target_n_min 未達、ポートフォリオ拡張検討",
    }

def explain_final_picks(filter_result: dict) -> str:
    """フィルタチェーン結果の人間可読サマリを生成(緩和段階を明示)"""
    lines = []
    lines.append(f"=== 5層フィルタチェーン結果 ===")
    lines.append(f"初期候補:      {filter_result['initial_count']:>6} 銘柄")
    lines.append(f"Layer 1 ハイブリッドPASS: {filter_result['layer_counts']['layer1_hybrid']:>6} 銘柄")
    lines.append(f"Layer 3 エントロピー: {'警告あり' if filter_result['layer3_warning'] else 'OK'}")
    stage = filter_result.get('relaxation_stage', 0)
    stage_label = {0: "緩和なし(全層適用)", 1: "緩和1(NLP除外)", 2: "緩和2(PCA+NLP除外)", 3: "全緩和でも未達"}.get(stage, str(stage))
    lines.append(f"緩和段階:      {stage} - {stage_label}")
    lines.append(f"最終候補数:    {filter_result['final_count']:>6} 銘柄")
    lines.append("")
    lines.append(f"最終銘柄: {', '.join(filter_result['final_picks'])}")
    if filter_result.get('warning'):
        lines.append(f"⚠ {filter_result['warning']}")
    return "\n".join(lines)

エンジニア的に言い換えると(多段階品質検査ライン + 緩和ロジック)

本コードは、製造業で言う 「多段階の品質検査ラインで不良品を順次除外していく + 検査基準が厳しすぎて全数 NG になったら段階的に緩める」作法と呼応します。Layer 1 で粗いフィルタ(ハイブリッドスコア)、Layer 2-4 で詳細フィルタ(PCA / エントロピー / NLP)、Layer 5 で最終選別(数の絞り込み)の階層構造に加え、relaxation_stage で「どの段階の検査を緩めて結果に至ったか」を可視化。「絶対値より変化率、突発より連続性」原則は、5層のうちどの層で銘柄が脱落したかの経時推移を観察することで運用に落とし込めます。

スニペット4:LINE 通知に運用ルール反映 + NISA / iDeCo 口座別自動判定最適化

応用編 #15 で構築した LINE Messaging API 連携を再活用し、月次/四半期のリバランスタイミングで最終ポートフォリオを通知します。v2 I1/I2 修正: #24 ポートフォリオ運用ルール(売却推奨/利確検討/比率超過)を LINE 通知に反映 + NISA / iDeCo 口座別最適化(4口座振り分け)を実装

# notify_hybrid_portfolio.py — ハイブリッド戦略の最終ポートフォリオを LINE に通知(v2 I1/I2)
# 動作環境: Python 3.11+ / requests 2.31+
import os
import requests
import pandas as pd
from datetime import date
from typing import Literal, TypedDict

LINE_API_BASE = "https://api.line.me/v2/bot/message/push"

AccountType = Literal["nisa_growth", "nisa_tsumitate", "ideco", "taxable"]

class AccountAllocation(TypedDict):
    nisa_growth: list[str]      # 新NISA成長枠(成長銘柄優先)
    nisa_tsumitate: list[str]   # 新NISAつみたて枠(インデックス代替)
    ideco: list[str]            # iDeCo(長期保有・配当重視)
    taxable: list[str]          # 課税口座(高配当 + 配当控除狙い)

def allocate_to_accounts(final_picks_df: pd.DataFrame,
                            life_stage: str = "40s_50s") -> AccountAllocation:
    """v2 I2: 4口座振り分けの自動判定最適化

    新NISA成長枠(年240万円): 成長スコア高 + キャピタルゲイン期待大
    新NISAつみたて枠(年120万円): 本記事は対象外(ETF/投信向け)
    iDeCo(職業別上限): 長期保有・配当重視(受給時非課税)
    課税口座: 高配当株(配当控除10-12.8%)

    判定ロジック(教育目的):
    - growth_score >= 1.5 (PASS寄り) かつ dividend_score < 1.5 → nisa_growth
    - dividend_score >= 1.5 かつ growth_score < 1.5 → taxable(配当控除狙い)
    - 両方高い → nisa_growth 優先(NISA は売却益も非課税)
    - 両方低い → ideco(長期保有で復活可能性に賭ける)
    """
    allocation: AccountAllocation = {
        "nisa_growth": [], "nisa_tsumitate": [], "ideco": [], "taxable": []
    }
    for _, row in final_picks_df.iterrows():
        ticker = row["ticker_normalized"]
        gs = row.get("growth_score", 1.0)
        ds = row.get("dividend_score", 1.0)
        if gs >= 1.5 and ds < 1.5:
            allocation["nisa_growth"].append(ticker)
        elif ds >= 1.5 and gs < 1.5:
            allocation["taxable"].append(ticker)
        elif gs >= 1.5 and ds >= 1.5:
            allocation["nisa_growth"].append(ticker)  # 両方高い → NISA優先
        else:
            allocation["ideco"].append(ticker)
    return allocation

def generate_action_recommendations(current_holdings: pd.DataFrame,
                                       new_picks: list[str]) -> dict:
    """v2 I1: #24 運用ルールに沿ったアクション推奨を生成

    返却:
    - sell_recommend: 損切候補(unrealized_pnl_pct <= -7%)
    - profit_take: 利確候補(unrealized_pnl_pct >= +20%)
    - position_warning: 比率超過(position_pct > 25%)
    - new_buy: 新規買付候補(new_picks にあって current にない銘柄)
    """
    recommendations = {
        "sell_recommend": [],
        "profit_take": [],
        "position_warning": [],
        "new_buy": [],
    }
    if "unrealized_pnl_pct" in current_holdings.columns:
        sell_mask = current_holdings["unrealized_pnl_pct"] <= -7.0
        recommendations["sell_recommend"] = current_holdings[sell_mask]["ticker_normalized"].tolist()
        profit_mask = current_holdings["unrealized_pnl_pct"] >= 20.0
        recommendations["profit_take"] = current_holdings[profit_mask]["ticker_normalized"].tolist()
    if "position_pct" in current_holdings.columns:
        pos_mask = current_holdings["position_pct"] > 25.0
        recommendations["position_warning"] = current_holdings[pos_mask]["ticker_normalized"].tolist()

    current_tickers = set(current_holdings["ticker_normalized"].tolist()) if not current_holdings.empty else set()
    recommendations["new_buy"] = [t for t in new_picks if t not in current_tickers]
    return recommendations

def format_portfolio_message(filter_result: dict,
                                allocation: AccountAllocation,
                                recommendations: dict,
                                life_stage: str,
                                risk_tolerance: str,
                                mix_weights: dict) -> str:
    """LINE 通知用メッセージ整形(v2 I1: 運用ルール反映、500文字以内目安)"""
    today = date.today().isoformat()
    lines = [
        f"【ハイブリッド戦略 月次リバランス {today}】",
        f"",
        f"プロファイル: {life_stage} / {risk_tolerance}",
        f"成長:配当 = {mix_weights['growth_weight']*100:.0f}%:{mix_weights['dividend_weight']*100:.0f}%",
        f"目標銘柄数: {mix_weights['n_stocks_min']}-{mix_weights['n_stocks_max']}",
        f"",
        f"■5層フィルタ結果(緩和段階 {filter_result.get('relaxation_stage', 0)})",
        f"  最終 {filter_result['final_count']} 銘柄: {', '.join(filter_result['final_picks'][:8])}",
    ]
    if filter_result.get('layer3_warning'):
        lines.append(f"  ⚠ エントロピー警告: 業種分散を再確認")

    # v2 I1: 運用ルール反映
    if recommendations.get("sell_recommend"):
        lines.append(f"")
        lines.append(f"■損切推奨 (-7%): {', '.join(recommendations['sell_recommend'])}")
    if recommendations.get("profit_take"):
        lines.append(f"■利確検討 (+20%): {', '.join(recommendations['profit_take'])}")
    if recommendations.get("position_warning"):
        lines.append(f"■比率超過 (25%): {', '.join(recommendations['position_warning'])}")
    if recommendations.get("new_buy"):
        lines.append(f"■新規候補: {', '.join(recommendations['new_buy'][:5])}")

    # v2 I2: 口座別最適化
    lines.append(f"")
    lines.append(f"■口座振り分け")
    if allocation["nisa_growth"]:
        lines.append(f"  NISA成長: {', '.join(allocation['nisa_growth'][:5])}")
    if allocation["taxable"]:
        lines.append(f"  課税(配当控除): {', '.join(allocation['taxable'][:5])}")
    if allocation["ideco"]:
        lines.append(f"  iDeCo: {', '.join(allocation['ideco'][:3])}")

    return "\n".join(lines)[:500]

def push_line_notification(message: str,
                              user_id: str | None = None,
                              channel_token: str | None = None) -> bool:
    """LINE Messaging API でプッシュ通知(応用編 #15 の再利用)"""
    token = channel_token or os.environ.get("LINE_CHANNEL_ACCESS_TOKEN")
    uid = user_id or os.environ.get("LINE_USER_ID")
    if not token or not uid:
        print("[警告] LINE 認証情報が未設定、環境変数を確認")
        return False

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }
    payload = {"to": uid, "messages": [{"type": "text", "text": message}]}
    try:
        resp = requests.post(LINE_API_BASE, json=payload, headers=headers, timeout=30)
        if resp.status_code != 200:
            print(f"[エラー] LINE 通知失敗: status={resp.status_code}, body={resp.text[:200]}")
        return resp.status_code == 200
    except requests.RequestException as e:
        print(f"[エラー] LINE 通知失敗: {e}")
        return False

エンジニア的に言い換えると(自動判定ループの完成 + 口座別最適化)

本コードは、製造業で言う 「日次/月次の自動レポートが定刻にダッシュボードに配信される運用フロー + 出荷先別の振り分けロジック」系譜を共有します。screen_parallel_v3 + ハイブリッド統合 + 5層フィルタ + 運用ルール反映 + 4口座最適化 + LINE 通知の連結で、「金曜深夜にスマホ通知が届く → 月曜朝に運用ルール推奨を確認 → NISA / iDeCo 口座別に発注」という自動判定運用ループが完成。「絶対値より変化率、突発より連続性」原則は、月次通知の前期比比較で運用に落とし込まれます。

設計判断の記録:ハイブリッド戦略の3判断 + 応用編〜発展編 #29 の俯瞰表(48件)

判断1:ハイブリッドスコアを線形重み付けか階層判定か(v2: composite 優先利用追加)

  • 採用理由: 線形重み付け + composite 優先利用のハイブリッド設計をデフォルト。代替案との比較:
    • 純粋な階層判定: 厳格だが抽出数が極端に減る
    • カテゴリ別ランキング併用: 実装複雑だが解釈性高
    • 採用:線形重み付け(prefer_composite=True で composite 優先): ライフステージ別の重み調整がシンプル、#24 C×A AND 設計の意図を尊重
  • 採用したことで失うもの: 「成長 FAIL でも配当 PASS で救済」という二極評価の精度
  • トリガー条件: 厳格運用 → 階層判定に切替、c+a+n の細部を見たい → prefer_composite=False
  • 残るメリット: ライフステージ別比率との整合性、解釈の容易さ、#24 設計の継承

判断2:ライフステージ判定を 3区分(20-30/40-50/60+)か 5区分か

  • 採用理由: 3区分 × リスク許容度3段階 = 9パターン。代替案との比較:
    • 5区分(10年刻み): 細かいが、20代と30代の本質的差異は小さく、過剰な細分化
    • 2区分(現役 vs 退職後): 単純すぎて 40-50代のバランス期を捉えられない
    • 採用:3 × 3 = 9 パターン: 実用上の判断軸として十分、過不足のないバランス
  • 採用したことで失うもの: 細かい年齢別最適化
  • トリガー条件: 個人事情で細かい調整が必要 → MixWeights を直接編集
  • 残るメリット: テーブル管理の容易さ、運用判断のシンプルさ

判断3:5層フィルタの段階的緩和(v2 で改善)

  • 採用理由: relaxation_stage 0→1→2 の段階的緩和、各段階の絞込結果を可視化。代替案との比較:
    • サイレント全捨て(v1): target_n_min 未満時に Layer 2/4 を全捨て、退化リスクが見えない
    • 厳格運用(緩和なし): 5層全適用で銘柄数 0 になる可能性
    • 採用:段階的緩和 + 可視化: 透明性を保ちつつ、実用的な銘柄数を確保
  • 採用したことで失うもの: コードの単純さ(緩和ロジックで複雑化)
  • トリガー条件: 厳格運用 → relaxation_stage=0 のみ受け付ける CAUTION 設定
  • 残るメリット: フォールバック挙動の透明性、運用判断材料

応用編 #13〜#21 + 発展編 #22〜#29 の主要設計判断 俯瞰表(48件、#30 で更に増える可能性あり)

記事主要判断採用
#13データソース選定J-Quants + EDINET
#14DB / アーキテクチャDuckDB + ELT + マスタ駆動
#14業種別補正設計industry_indicator_map + direction
#14正規化キーticker_normalized 5桁0埋め
#15並列化技術multiprocessing initializer
#15通知メディアLINE Messaging API
#15スケジューラcron → GitHub Actions
#16連続増配判定連続非減少
#16EPS 安定性指標変動係数 CV + 線形回帰
#16新規上場銘柄の扱い5期未満は CAUTION 強制
#17異常値検知IQR + Z + SPC + WER の3-4層
#17市場全体ショック対応絶対値 + TOPIX 相対値併用
#18業種粒度10→25業種
#18業種別主指標数1業種1主指標
#18マスタ更新サイクル四半期レビュー
#18テーブル PK 設計複合PK / 単列PK 使い分け
#19集中度指標HHI 2000/3000
#19相関分析期間過去24ヶ月 + 直近6ヶ月
#19FMEA RPN 重み等倍積(S × O × D)
#19ポートフォリオテーブル個別銘柄評価とは別テーブル
#20master_runner 方式NotImplementedError 委譲
#20業種カバレッジ25業種で時価総額9割
#21成長株フレームワークCAN-SLIM
#21パイプライン再利用応用編流用、判定層拡張
#21市場カバレッジ日米両市場(FMP + SEC EDGAR)
#22YoY 計算ロジック絶対値分母
#22連続性判定min_consecutive=2
#23CAGR 期間優先順位5年優先 + 3年フォールバック
#23赤字期 CAGR 扱いNone で CAUTION
#23可視化粒度PASS 銘柄のみ詳細
#24複合戦略デフォルトAND(OR/重み付け 切替可)
#249セル表示粒度9セル全表示
#24バックテスト粒度概念実装のみ
#25テキスト分析手法簡易キーワード(LLM 切替可)
#25N 4観点の統合テキスト × 高値の2軸 AND
#25株価/ファンダのバランス両軸併用
#26PCA 入力特徴量定量10個
#26標準化方法StandardScaler
#26採用主成分数累積寄与率80% + Scree Plot
#27対数の底自然対数(log2 切替可)
#27判定閾値銘柄数 tier 別
#27シェア計算ベース時価総額加重デフォルト
#28NLP 手法デフォルトSentence-BERT 多言語
#28多言語対応共通モデルで日英統一
#28類似度閾値手法別固定
#29ハイブリッドスコア統合線形重み付け + composite 優先利用
#29ライフステージ区分3区分 × リスク許容度3段階 = 9パターン
#295層フィルタ + 段階的緩和relaxation_stage 0→1→2 で透明化

本業の話:マルチ製品ライン戦略でリスク分散と安定キャッシュフローを両立

筆者が研究開発部門でマルチ製品ライン戦略を担当していたとき、ベテランから次の指導を受けました:

  • 主力単独に賭けるな。新規開発の主力製品は需要変動が大きく、量産失敗時のキャッシュフロー打撃が致命的になる」
  • 主力と安定の組合せ。高利益・少量の主力製品と、薄利・多量の安定製品を併走させて、全体のキャッシュフロー変動を抑える」
  • ライフサイクルで比率調整。事業立ち上げ期は主力寄り、成熟期は安定寄り、衰退期は次世代主力にシフト。投資のライフステージ別ミックス比率と等価の構造

具体的な業務インパクトと4年スパンのマイルストーン(v2 D1: サンプル数の単位を明確化、v2 D2: 4年目に R&D 投資維持率を追加):

  • 1年目(指摘): 初稿は主力製品単一ラインで、需要変動時の月次キャッシュフロー変動が ±40% と激しい。ベテラン指摘でマルチライン戦略導入
  • 2年目(マルチライン導入): 主力1ライン + 安定2ラインの3ライン体制に移行、生産能力を主力40% / 安定60% で配分。月次キャッシュフロー変動が ±15% に低下(前年比 -62%)(測定方法: 過去2年分の月次キャッシュフロー実績の標準偏差を新基準と旧基準で比較。サンプル: 月次データ24点(2年分)。内訳: 主力ライン由来変動 ±25% → ±10% / 安定ライン由来変動 ±5%)
  • 2.5年目(市場環境別動的調整): 「絶対値より変化率」原則を実装、四半期ごとに市場需要動向を見て主力:安定の比率を 30:70 〜 50:50 で動的調整、本業閾値 ΔCV=±10% で大幅調整実施
  • 3年目(運用): マルチライン戦略 + 動的調整が経営会議の標準資料に。「絶対値より変化率、突発より連続性」原則のキャッシュフロー版が部内浸透
  • 4年目(成果): 市場ショック(特定セグメントの急減)時にも事業全体のキャッシュフローが安定、R&D 投資維持率が前年比 +5% で計画通り継続(v2 D2 追加。測定方法: R&D 予算実績 ÷ 計画値の比率を新旧基準で比較。サンプル: 4年分のR&D投資案件 約12件)。安定ラインがクッションとして機能した実証

本記事のハイブリッド戦略は、この本業のマルチ製品ライン戦略経験と同じ作法です。製造業の「主力 + 安定の組合せでキャッシュフロー変動を抑える」が、投資の「成長 + 配当の組合せでポートフォリオ変動を抑える」に変換できるのが、本記事の核心的な発見です。

逆方向の転移:投資のライフステージ別ミックス比率が本業の事業ライフサイクル管理を強化(架空シナリオ)

本記事のライフステージ別ミックス比率を、本業の事業ライフサイクル管理に逆輸入する架空シナリオで具体化します:

  • 当初状態: 本業の事業ライフサイクル別の主力:安定比率は経営者の経験と勘で都度決定、再現性に欠ける
  • 投資の発想を逆輸入: ライフステージ × リスク許容度 = 9パターンのテーブル化を本業に応用 → 事業フェーズ × 市場環境 = 9パターンの主力:安定比率テーブルを構築
  • 改善後: 経営判断の説明可能性が向上、新任経営層への引継ぎ時間が前年比 -50%。テーブル化により会議の議論が「比率の妥当性」から「テーブルへのフィードバック」に建設化
  • 双方向の効果: 投資のライフステージ別比率と本業の事業フェーズ別比率が相互に改善。応用編で確立した双方向の知識循環がハイブリッド戦略でも続いています

まとめ:ハイブリッド戦略で配当 × 成長を統合、応用編+発展編の集約点

  • ハイブリッド戦略は配当 × 成長を統合し、ライフステージ別ミックス比率で運用する Python 自動判定の集約点。線形重み付け + composite 優先利用でハイブリッドスコア(0-10)を計算、ライフステージ × リスク許容度の9パターンテーブルで成長:配当の重みを動的調整、5層フィルタチェーン + 段階的緩和(relaxation_stage 0→1→2 透明化)で 5-8銘柄に絞り込み、#24 運用ルール(損切 -7% / 利確 +20%)を LINE 通知に反映、新NISA成長枠 / 課税口座 / iDeCo の4口座最適化で税効率向上
  • 応用編 + 発展編の全要素統合: #15 並列スクリーニング + #19 HHI + #24 ポートフォリオ運用ルール + #26 PCA + #27 情報エントロピー + #28 NLP の集約点
  • 「絶対値より変化率、突発より連続性」原則の3階層: 個別判定(#22-#28)→ ポートフォリオ判定(#29 ハイブリッド + 段階的緩和)→ 経時運用(月次リバランス + 運用ルール反映)
  • 機械の自動判定 PASS は「成長株候補の最有力ゾーン」、購入候補ではない: ライフステージ別ミックス比率と NISA / iDeCo 振り分けは教育目的の参考値、最終運用は個別事情で調整。#24 のポートフォリオ運用ルールと #27 ポートフォリオ多様性判定を併用してください

今日からできる3つのアクション

  1. 本記事スニペット1〜2 を実装し、自分のライフステージ × リスク許容度から成長:配当のミックス比率を取得。ハイブリッドスコアの自動判定で #24 C×A 複合判定結果と#15 高配当判定結果がどう統合されるか、composite 優先 vs 単純平均の差を確認
  2. スニペット3 で 5層フィルタチェーン + 段階的緩和を適用し、explain_final_picks で各層の絞り込み件数と relaxation_stage を観察、どの層がボトルネックか把握
  3. スニペット4 で 運用ルール反映 + NISA / iDeCo 4口座最適化 + LINE 通知の動作確認。抽出後の人手最終判定手順として:
    • 5-8銘柄の業種分散確認: 応用編 #19 HHI と #27 情報エントロピーで再評価
    • ライフステージ別比率の自分用カスタマイズ: MixWeights テーブルを直接編集
    • NISA 成長枠 / つみたて枠 / iDeCo / 課税口座の自動振り分け確認: allocate_to_accounts の出力を実際の証券口座と照合
    • 3ヶ月後にハイブリッドスコア再計算: 前期比変動を観察、構造変化があれば見直し
    • #24 ポートフォリオ運用ルール適用: 5-8銘柄、損切 -7〜-8%・利確 +20-25%・1銘柄上限 20-25%(v2 で LINE 通知に自動反映)

次回予告:シリーズ総括 — 30記事の学びと「製品開発DXエンジニアの投資術」の集大成

次回(記事#30)はシリーズ最終回です。30記事を通じた学びの集大成として、(1) 基礎編(インデックス投資 × DX思想)、(2) 応用編(高配当株 × データパイプライン)、(3) 発展編(CAN-SLIM × アーキテクチャ設計力)の3編を貫く「製品開発DXエンジニアの投資術」の核心を整理します。応用編 + 発展編の設計判断俯瞰(48以上)、本業 × 投資の双方向知識循環、今後の発展方向(バックテスト・LLM 統合・国際分散)への展望まで。

HowTo schema 実装サンプル(v2 S3: step.text 拡充)

{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "ハイブリッド戦略|配当 × 成長 を Python 自動判定でポートフォリオ統合",
  "description": "応用編 + 発展編の全要素を統合し、ライフステージ別ミックス比率と5層フィルタ + 段階的緩和、NISA / iDeCo 4口座最適化、月次リバランスのLINE通知まで完成させる集約点",
  "totalTime": "PT3H",
  "tool": [
    {"@type": "HowToTool", "name": "Python 3.11+"},
    {"@type": "HowToTool", "name": "pandas 2.x / scikit-learn 1.4+"},
    {"@type": "HowToTool", "name": "LINE Messaging API"},
    {"@type": "HowToTool", "name": "新NISA / iDeCo 口座(任意)"}
  ],
  "step": [
    {"@type": "HowToStep", "name": "ハイブリッドスコア自動計算",
     "text": "高配当 PASS と CAN-SLIM C×A×N PASS を線形重み付け + composite 優先利用で 0-10 スコアに統合する自動判定。prefer_composite=True がデフォルト"},
    {"@type": "HowToStep", "name": "ライフステージ別ミックス比率",
     "text": "9パターンテーブル(3ライフステージ × 3リスク許容度)から自分の成長:配当 比率を自動取得、市場環境(強気/弱気)の動的調整も併用"},
    {"@type": "HowToStep", "name": "5層フィルタ + 段階的緩和",
     "text": "ハイブリッド + PCA + エントロピー + NLP の5層フィルタで数百候補から 5-8銘柄に絞り込み、relaxation_stage で緩和段階を可視化"},
    {"@type": "HowToStep", "name": "運用ルール反映 + 4口座最適化 + LINE 通知",
     "text": "#24 損切/利確/比率超過を LINE 通知に反映、NISA成長枠/つみたて枠/iDeCo/課税口座の4口座振り分けで税効率向上、月次リバランスを自動化"}
  ]
}

「製品開発DXエンジニアの投資術」シリーズ全体像

本記事は 発展編(記事#21〜#30)の第9回 — 応用編+発展編の集約点(最終実装) です。

  • 基礎編(#01〜#10): 完了
  • 応用編(#11〜#20): 完了 ✅
  • 発展編(#21〜#30): 進行中

発展編 ロードマップ:

前回 #28 NLP で N要素高度化本記事 #29 ハイブリッド戦略 | 次回 #30(シリーズ総括、公開予定)

関連記事(応用編から): #15 全銘柄スクリーニング#16 配当推移の安定性#17 異常値検知#19 業種分散 FMEA#20 応用編まとめ

免責事項(再掲)

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。ハイブリッド戦略のミックス比率・ライフステージ別の配分・5層フィルタの閾値・NISA / iDeCo の口座別最適化ロジックは教育目的の参考値であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants・FMP・LINE Messaging API の利用規約は変更される可能性があるため、実装時は各サービスの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants/FMP から取得した個別銘柄の実値・株価実値は掲載していません。LINE 認証情報・OpenAI API キー等はコードに直書きせず環境変数で管理してください。NISA / iDeCo の制度詳細・年間上限額は2026年5月時点のもので、税制改正により変更される可能性があるため、運用時は最新の制度を確認してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次