株主優待廃止新設

はじめに なぜ「株主優待イベント」は投資機会なのか

株主優待の廃止発表新設発表は、企業業績とは別に市場の需給やセンチメントに大きな歪みを生み出します。発表直後は情報が行き渡らず、機関投資家・個人投資家のポジション調整が進むため、短期的に大きな価格変動が発生しやすいのが特徴です。

本記事では、発表日の翌営業日から数日間のリアクションウインドウに着目し、日次リターンを計測、さらに累積リターンとリスク調整後パフォーマンス指標であるシャープレシオを一気に算出するPythonサンプルスクリプトを紹介します。前半でコード全文と解説、後半で応用アイデア&戦略を提示しますので、ぜひご活用ください。

1. サンプルスクリプト全文

発表翌日の Day+1Day+3 までの価格反応を測定し、累積リターンとシャープレシオをまとめて算出・可視化します。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
株主優待発表リアクション分析+累積リターン・シャープレシオ
──────────────────────────────────────────────
* 発表日の翌営業日から N 営業日間のリターンを日次で計測
* 累積リターンとシャープレシオも算出
"""

from datetime import timedelta
from typing import List
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import numbers
from matplotlib import font_manager as fm

# Mac用日本語フォント設定
font_path = "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc"
jp_font   = fm.FontProperties(fname=font_path)
plt.rcParams['font.family'] = jp_font.get_name()

# イベント定義 (announce_date, event_type, ticker)
EVENTS = [
    ("2023-10-26", "abolish",   "8697.T"),  # JPX 優待廃止
    ("2024-12-11", "abolish",   "2695.T"),  # くら寿司 廃止
    ("2025-02-19", "introduce", "2695.T"),  # くら寿司 新設
    ("2025-03-03", "introduce", "7203.T"),  # トヨタ自動車 新設
]

WINDOW_DAYS = 3  # Day+1~Day+3

def _scalar(val) -> float:
    if isinstance(val, numbers.Number):
        return float(val)
    if isinstance(val, (pd.Series, pd.DataFrame)):
        return float(val.squeeze())
    return float(val[0])

def _fetch_price(ticker: str, start, end) -> pd.Series:
    try:
        df = yf.download(ticker, start=start, end=end, progress=False)
    except Exception:
        return pd.Series(dtype=float)
    if df is None or df.empty:
        return pd.Series(dtype=float)
    col = "Adj Close" if "Adj Close" in df.columns else "Close"
    s = df[col].copy(); s.name = "close"
    return s.asfreq("B")

def main() -> None:
    df_evt = pd.DataFrame(EVENTS, columns=["announce","type","ticker"])
    df_evt["announce"] = pd.to_datetime(df_evt["announce"])
    start_min = df_evt["announce"].min() - timedelta(days=WINDOW_DAYS*3)
    end_max   = df_evt["announce"].max() + timedelta(days=WINDOW_DAYS*3)
    bench = _fetch_price("^N225", start_min, end_max)
    records: List[dict] = []

    for _, row in df_evt.iterrows():
        ann = row["announce"]; tk = row["ticker"]
        px  = _fetch_price(tk, ann - timedelta(days=WINDOW_DAYS*3),
                              ann + timedelta(days=WINDOW_DAYS*3))
        if px.empty: continue
        ann_idx   = px.index.get_indexer([ann], method="nearest")[0]
        start_idx = min(ann_idx+1, len(px)-1)
        for d in range(1, WINDOW_DAYS+1):
            i0 = start_idx; i1 = min(ann_idx+d, len(px)-1)
            ret = (_scalar(px.iloc[i1]) / _scalar(px.iloc[i0]) - 1) * 100
            b0 = _scalar(bench.loc[px.index[i0]]); b1 = _scalar(bench.loc[px.index[i1]])
            alpha = ret - ((b1 / b0 - 1) * 100)
            records.append({
                "ticker":   tk,
                "type":     row["type"],
                "announce": ann.date(),
                "day":      f"Day+{d}",
                "ret_%":    ret,
                "alpha_%":  alpha
            })

    df_res = pd.DataFrame(records)
    pivot_ret   = df_res.pivot_table("ret_%",   index="day", columns="type", aggfunc="mean").round(2)
    pivot_alpha = df_res.pivot_table("alpha_%", index="day", columns="type", aggfunc="mean").round(2)

    # 累積リターン・シャープレシオ計算
    df_res["ret_frac"] = df_res["ret_%"] / 100
    cum = (df_res
           .groupby(["type","announce"])["ret_frac"]
           .apply(lambda x: (1+x).prod()-1)
           .reset_index(name="cum_ret"))
    sharpe = (cum
              .groupby("type")["cum_ret"]
              .agg(mean="mean", std="std")
              .assign(sharpe=lambda x: x["mean"]/x["std"])
              .round(4))

    print("平均リターン (%)\n", pivot_ret)
    print("\n平均超過リターン (α) (%)\n", pivot_alpha)
    print("\n累積リターン & シャープレシオ\n", sharpe)

    fig, axes = plt.subplots(1, 3, figsize=(16,4))
    pivot_ret.plot(kind="bar",   ax=axes[0], title="平均リターン %")
    pivot_alpha.plot(kind="bar", ax=axes[1], title="平均α %")
    sharpe[["mean","std"]].plot(kind="bar", ax=axes[2], title="累積リターン mean/std")
    axes[2].set_ylabel("累積リターン")
    plt.tight_layout(); plt.show()

if __name__=="__main__":
    main()

優待新設廃止

2. コード解説:処理フローを紐解く

2.1 リアクションウインドウ取得

発表日が休日・土日に当たる場合も、px.index.get_indexer([announce], method="nearest") で最寄り営業日を取得し、翌営業日の Day+1 から反応を測定します。

2.2 日次リターン&超過リターン計算

  • ret_%:Day+1 始値と Day+n 終値の単純リターン(%)
  • alpha_%:日経平均同期間リターンとの差分でイベント効果を抽出

2.3 累積リターン算出

日次リターンを掛け合わせ (1+r1)*(1+r2)*… - 1 でイベントウインドウ全体の合成パフォーマンスを計算。

2.4 シャープレシオ計算

累積リターンの平均÷標準偏差によって、リスク調整後パフォーマンスを評価。リスクフリーレートはゼロと仮定しています。

3. 応用アイデア:さらに深掘りする3つの案

3.1 可変ウインドウ&最適日探索

WINDOW_DAYS を複数値でループし、Day+1~Day+N の最適パフォーマンスをヒートマップ化。発表後の短期・中期で最適保有期間を自動探索します。

3.2 出来高&ボラティリティフィルタとの組み合わせ

優待発表前後の出来高急増やオプションのIV急騰をトリガーに、情報感度の高い銘柄群だけを抽出し同様分析。どの条件で反応が強いか定量比較できます。

3.3 自動レポート&Slack通知パイプライン化

スクリプト実行後、df_ressharpeをCSV/Excelに出力し、図表をPDF化、Slack APIでチームに共有する自動ワークフローを構築できます。

まとめ:データで検証するイベント投資の強み

株主優待の廃止・新設は需給歪みを生む稀有なイベントです。このサンプルスクリプトで発表翌営業日からの短期反応を可視化し、累積パフォーマンスおよびシャープレシオまで算出すれば、ルールの有効性を客観的に判断できます。自分の投資戦略に合わせてウインドウ幅やフィルタを調整し、再現性の高いイベント投資を実践しましょう。

Pythonを学ぶなら「Udemy」で効率的に

独学でPythonを学ぶ場合、公式リファレンスだけでは挫折しがちです。
そんな時に心強いのが、動画で体系的に学べるUdemyの講座

  • Python初心者向け講座から、投資データ分析に特化した実践講座まで多数
  • 買い切り型で、一度買えばいつでも復習可能
  • セール時は90%オフなど、非常にお得

投資家にとっての“自己投資”として、Udemyは非常にコスパの高い選択肢です。

【裏ワザ】Udemyの講座は“ある方法”でさらにお得に買える

実は、Udemyの講座はポイントサイトを経由することで、さらにお得に購入できることをご存知ですか?

おすすめは「ハピタス」というポイントサイト。
ハピタスを経由してUdemyで講座を購入すると、購入金額の数15%がポイントとして還元されます(2025年4月調査時点)。

手順は3ステップ

  1. ハピタスに無料登録
  2. その買うを、もっとハッピーに。|ハピタス

  3. 「Udemy」と検索して表示されたリンクをクリック
  4. 講座を通常通り購入するだけ

講座はPythonの基本から株価分析、AI株価予測まで、いろいろそろっています。

Udemy株価分析講座

学びながらポイントも貯まり、一石二鳥です。