先日、持ち株のある製造メーカーの決算発表があったんですが、前日に「いい決算が出そう」というなんとなくの感触だけで買い増したら、見事にサプライズ下落してやられました。。。損切りしながら「これ、ちゃんとデータで検証できないか」と思ったのが今回のきっかけです。子育てで相場を見る時間がほぼない僕にとって、決算前後の動きをルール化してバックテストで検証できれば、感情に任せた判断ミスが減るはず→そう思って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から取得できる)を組み込んで、もう少し精度を上げることに挑戦してみるつもりです。また爆死しても笑い話にできるよう、まずは少額で試します(笑)。

