機械学習モデル(LSTM/XGBoost/強化学習)による株価予測と売買戦略

機械学習ディープラーニング時系列予測強化学習Pythonバックテスト

戦略概要

機械学習モデルを活用した株価予測は、時系列予測・シグナル分類・取引最適化で従来手法を上回る精度を示す。本記事では、LSTM/GRU、XGBoost/LightGBM、強化学習(DQN/PPO)、CNNの4つのアプローチを網羅し、アンサンブル手法と実運用での課題まで解説する。

各モデルの特徴と適用領域

モデル適用領域強み弱み
LSTM/GRU時系列予測長期依存性の学習、リターン予測過学習しやすい、計算コスト高
XGBoost/LightGBMシグナル分類特徴量重要度の解釈性、高速時系列順序を考慮しない
DQN/PPO最適執行・ポジション管理動的な意思決定、スリッページ低減学習に大量データ必要
CNNチャートパターン認識画像ベースで直感的データ前処理が複雑

LSTM/GRUによる時系列予測

結論

Attention機構を統合したLSTM/GRUハイブリッドモデルが2025年時点での主流。Transformer-LSTMは従来LSTMに比べ予測精度を18%向上させ、日経225予測でRMSE 400ポイント以内を達成。

モデル構造

import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, Model

def build_attention_lstm(
    sequence_length: int,
    n_features: int,
    lstm_units: int = 64
) -> Model:
    """Attention付きLSTMモデル"""

    # 入力
    inputs = layers.Input(shape=(sequence_length, n_features))

    # LSTM層(return_sequences=Trueでアテンション適用可能)
    lstm_out = layers.LSTM(lstm_units, return_sequences=True)(inputs)
    lstm_out = layers.Dropout(0.2)(lstm_out)
    lstm_out = layers.LSTM(lstm_units // 2, return_sequences=True)(lstm_out)

    # Self-Attention
    attention = layers.MultiHeadAttention(
        num_heads=4,
        key_dim=lstm_units // 4
    )(lstm_out, lstm_out)
    attention = layers.GlobalAveragePooling1D()(attention)

    # 出力層
    outputs = layers.Dense(32, activation="relu")(attention)
    outputs = layers.Dense(1)(outputs)  # 予測リターン

    model = Model(inputs, outputs)
    model.compile(optimizer="adam", loss="mse", metrics=["mae"])

    return model

# モデル作成例
model = build_attention_lstm(
    sequence_length=60,  # 過去60日
    n_features=10        # OHLCV + テクニカル指標
)

特徴量設計

特徴量カテゴリ具体例説明
価格系OHLCV始値・高値・安値・終値・出来高
テクニカルRSI, MACD, ボリンジャーバンドモメンタム・トレンド指標
ボラティリティATR, 実現ボラティリティリスク指標
外部要因USD/JPY, VIX為替・恐怖指数
センチメントニュースNLPスコアテキスト解析結果
import pandas as pd
import talib

def create_features(df: pd.DataFrame) -> pd.DataFrame:
    """LSTMモデル用特徴量を作成"""

    features = df[["Open", "High", "Low", "Close", "Volume"]].copy()

    # テクニカル指標
    features["RSI"] = talib.RSI(df["Close"], timeperiod=14)
    features["MACD"], features["MACD_signal"], _ = talib.MACD(df["Close"])
    features["BB_upper"], features["BB_middle"], features["BB_lower"] = talib.BBANDS(df["Close"])

    # リターン
    features["returns_5d"] = df["Close"].pct_change(5)
    features["returns_20d"] = df["Close"].pct_change(20)

    # ボラティリティ
    features["ATR"] = talib.ATR(df["High"], df["Low"], df["Close"], timeperiod=14)

    # 正規化
    for col in features.columns:
        features[col] = (features[col] - features[col].mean()) / features[col].std()

    return features.dropna()

XGBoost/LightGBMによるシグナル分類

結論

XGBoost/LightGBMは株価シグナル(Buy/Sell/Hold)の分類で精度85-87%を達成。特徴量エンジニアリングが精度を大きく左右し、ラグ変数・テクニカル指標・ファンダメンタルズの組み合わせが有効。

特徴量エンジニアリング

import lightgbm as lgb
import xgboost as xgb
from sklearn.model_selection import TimeSeriesSplit

def create_classification_features(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
    """シグナル分類用特徴量を作成"""

    features = pd.DataFrame()

    # ラグ変数(過去n日のリターン)
    for lag in [1, 3, 5, 10, 20]:
        features[f"return_lag_{lag}"] = df["Close"].pct_change(lag)

    # テクニカル指標
    features["RSI"] = talib.RSI(df["Close"], timeperiod=14)
    features["stoch_k"], features["stoch_d"] = talib.STOCH(
        df["High"], df["Low"], df["Close"]
    )
    features["ADX"] = talib.ADX(df["High"], df["Low"], df["Close"], timeperiod=14)

    # ボリンジャーバンドからの乖離
    bb_upper, bb_middle, bb_lower = talib.BBANDS(df["Close"])
    features["bb_position"] = (df["Close"] - bb_lower) / (bb_upper - bb_lower)

    # 出来高変化
    features["volume_change"] = df["Volume"].pct_change()
    features["volume_ma_ratio"] = df["Volume"] / df["Volume"].rolling(20).mean()

    # ターゲット:5日後リターンで分類
    future_return = df["Close"].shift(-5) / df["Close"] - 1
    target = pd.cut(
        future_return,
        bins=[-np.inf, -0.02, 0.02, np.inf],
        labels=["sell", "hold", "buy"]
    )

    return features.dropna(), target.loc[features.dropna().index]

def train_lightgbm_classifier(
    X: pd.DataFrame,
    y: pd.Series,
    n_splits: int = 5
) -> lgb.LGBMClassifier:
    """LightGBM分類器をウォークフォワードで学習"""

    tscv = TimeSeriesSplit(n_splits=n_splits)

    params = {
        "objective": "multiclass",
        "num_class": 3,
        "learning_rate": 0.05,
        "num_leaves": 31,
        "max_depth": 6,
        "min_child_samples": 20,
        "subsample": 0.8,
        "colsample_bytree": 0.8,
        "random_state": 42,
        "n_estimators": 200
    }

    model = lgb.LGBMClassifier(**params)

    # 最後のfoldで学習
    for train_idx, val_idx in tscv.split(X):
        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]

    model.fit(
        X_train, y_train,
        eval_set=[(X_val, y_val)],
        callbacks=[lgb.early_stopping(50)]
    )

    return model

特徴量重要度分析

import matplotlib.pyplot as plt

def analyze_feature_importance(model: lgb.LGBMClassifier, feature_names: list[str]) -> pd.DataFrame:
    """特徴量重要度を分析"""

    importance_df = pd.DataFrame({
        "feature": feature_names,
        "importance": model.feature_importances_
    }).sort_values("importance", ascending=False)

    return importance_df

# 使用例
# importance = analyze_feature_importance(model, X.columns.tolist())
# print(importance.head(10))

強化学習(DQN/PPO)による最適執行

結論

DQNは大口注文のスリッページを13%低減、PPOはポジション管理でリスク調整後リターンを15%向上。報酬関数としてSharpe RatioやCalmar Ratioを採用。

DQN実装

import gymnasium as gym
from stable_baselines3 import DQN
import numpy as np

class TradingEnv(gym.Env):
    """トレーディング環境"""

    def __init__(self, prices: np.ndarray, features: np.ndarray):
        super().__init__()

        self.prices = prices
        self.features = features
        self.n_steps = len(prices)

        # アクション: 0=何もしない, 1=買い, 2=売り
        self.action_space = gym.spaces.Discrete(3)

        # 状態: 特徴量 + ポジション
        self.observation_space = gym.spaces.Box(
            low=-np.inf,
            high=np.inf,
            shape=(features.shape[1] + 1,),
            dtype=np.float32
        )

        self.reset()

    def reset(self, seed=None):
        super().reset(seed=seed)
        self.current_step = 0
        self.position = 0  # -1: ショート, 0: なし, 1: ロング
        self.entry_price = 0
        self.total_reward = 0
        return self._get_observation(), {}

    def _get_observation(self) -> np.ndarray:
        obs = np.concatenate([
            self.features[self.current_step],
            [self.position]
        ])
        return obs.astype(np.float32)

    def step(self, action: int):
        current_price = self.prices[self.current_step]
        reward = 0

        # アクション実行
        if action == 1 and self.position <= 0:  # 買い
            if self.position == -1:  # ショートをクローズ
                reward = (self.entry_price - current_price) / self.entry_price
            self.position = 1
            self.entry_price = current_price

        elif action == 2 and self.position >= 0:  # 売り
            if self.position == 1:  # ロングをクローズ
                reward = (current_price - self.entry_price) / self.entry_price
            self.position = -1
            self.entry_price = current_price

        self.current_step += 1
        self.total_reward += reward

        terminated = self.current_step >= self.n_steps - 1
        truncated = False

        return self._get_observation(), reward, terminated, truncated, {}

def train_dqn_agent(env: TradingEnv, total_timesteps: int = 100000) -> DQN:
    """DQNエージェントを学習"""

    model = DQN(
        "MlpPolicy",
        env,
        learning_rate=1e-4,
        buffer_size=50000,
        learning_starts=1000,
        batch_size=32,
        gamma=0.99,
        exploration_fraction=0.2,
        exploration_final_eps=0.05,
        verbose=1
    )

    model.learn(total_timesteps=total_timesteps)

    return model

PPOによるポジション管理

from stable_baselines3 import PPO

def train_ppo_agent(env: TradingEnv, total_timesteps: int = 100000) -> PPO:
    """PPOエージェントを学習(ポジション管理向け)"""

    model = PPO(
        "MlpPolicy",
        env,
        learning_rate=3e-4,
        n_steps=2048,
        batch_size=64,
        n_epochs=10,
        gamma=0.99,
        gae_lambda=0.95,
        clip_range=0.2,
        ent_coef=0.01,
        verbose=1
    )

    model.learn(total_timesteps=total_timesteps)

    return model

CNNによるチャートパターン認識

結論

ResNetアーキテクチャを採用したCNNは、ヘッドアンドショルダー等のパターン検出率92%を達成。日本株ローソク足チャートのトレンド転換予測で精度85%。

実装

from tensorflow.keras import layers, Model
import mplfinance as mpf
from PIL import Image
import io

def create_chart_image(
    df: pd.DataFrame,
    width: int = 224,
    height: int = 224
) -> np.ndarray:
    """ローソク足チャートを画像に変換"""

    # mplfinanceでチャート作成
    buf = io.BytesIO()
    mpf.plot(
        df,
        type="candle",
        volume=True,
        savefig=dict(fname=buf, dpi=100, bbox_inches="tight"),
        style="charles"
    )
    buf.seek(0)

    # 画像読み込み・リサイズ
    img = Image.open(buf).convert("RGB")
    img = img.resize((width, height))

    return np.array(img) / 255.0

def build_chart_cnn(input_shape: tuple = (224, 224, 3), num_classes: int = 3) -> Model:
    """チャートパターン認識用CNN(ResNetベース)"""

    base_model = tf.keras.applications.ResNet50V2(
        weights="imagenet",
        include_top=False,
        input_shape=input_shape
    )

    # ファインチューニング: 最後の10層のみ学習
    for layer in base_model.layers[:-10]:
        layer.trainable = False

    x = base_model.output
    x = layers.GlobalAveragePooling2D()(x)
    x = layers.Dense(256, activation="relu")(x)
    x = layers.Dropout(0.5)(x)
    outputs = layers.Dense(num_classes, activation="softmax")(x)

    model = Model(inputs=base_model.input, outputs=outputs)
    model.compile(
        optimizer=tf.keras.optimizers.Adam(1e-4),
        loss="categorical_crossentropy",
        metrics=["accuracy"]
    )

    return model

アンサンブル手法

結論

LSTM + XGBoost + RLのスタッキングで予測精度を20%向上、2020-2025年のバックテストで年平均リターン11%(単独モデル比+4%)を達成。

スタッキング実装

from sklearn.linear_model import LogisticRegression

def ensemble_predictions(
    lstm_pred: np.ndarray,
    xgb_pred: np.ndarray,
    rl_action: np.ndarray,
    weights: list[float] = None
) -> np.ndarray:
    """複数モデルの予測を統合"""

    if weights is None:
        weights = [0.4, 0.4, 0.2]  # LSTM, XGBoost, RL

    # 重み付け平均(単純アンサンブル)
    ensemble = (
        weights[0] * lstm_pred +
        weights[1] * xgb_pred +
        weights[2] * rl_action
    )

    return ensemble

class StackingEnsemble:
    """スタッキングアンサンブル"""

    def __init__(self):
        self.meta_learner = LogisticRegression()

    def fit(
        self,
        lstm_pred: np.ndarray,
        xgb_pred: np.ndarray,
        cnn_pred: np.ndarray,
        y_true: np.ndarray
    ):
        """メタ学習器を学習"""

        X_meta = np.column_stack([lstm_pred, xgb_pred, cnn_pred])
        self.meta_learner.fit(X_meta, y_true)

    def predict(
        self,
        lstm_pred: np.ndarray,
        xgb_pred: np.ndarray,
        cnn_pred: np.ndarray
    ) -> np.ndarray:
        """統合予測を生成"""

        X_meta = np.column_stack([lstm_pred, xgb_pred, cnn_pred])
        return self.meta_learner.predict(X_meta)

ウォークフォワード検証

結論

ウォークフォワード検証(WFO)適用でSharpe Ratioが1.2から1.7に向上。過学習を防ぎ、実運用パフォーマンスとの乖離を縮小。

実装

from dataclasses import dataclass
from typing import Generator

@dataclass
class WFOResult:
    """ウォークフォワード検証結果"""
    train_start: str
    train_end: str
    test_start: str
    test_end: str
    sharpe_ratio: float
    total_return: float
    max_drawdown: float

def walk_forward_split(
    df: pd.DataFrame,
    train_period: int = 252,  # 1年
    test_period: int = 63,     # 四半期
    step: int = 63             # ステップ
) -> Generator[tuple[pd.DataFrame, pd.DataFrame], None, None]:
    """ウォークフォワード分割"""

    for start in range(0, len(df) - train_period - test_period, step):
        train_end = start + train_period
        test_end = train_end + test_period

        train_df = df.iloc[start:train_end]
        test_df = df.iloc[train_end:test_end]

        yield train_df, test_df

def run_walk_forward_validation(
    df: pd.DataFrame,
    model_fn: callable
) -> list[WFOResult]:
    """ウォークフォワード検証を実行"""

    results = []

    for train_df, test_df in walk_forward_split(df):
        # モデル学習
        model = model_fn(train_df)

        # テスト期間で評価
        predictions = model.predict(test_df)

        # パフォーマンス計算(簡略化)
        returns = test_df["Close"].pct_change().dropna()
        strategy_returns = returns * np.sign(predictions[:-1])

        sharpe = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252)
        total_return = (1 + strategy_returns).prod() - 1
        max_dd = (strategy_returns.cumsum() - strategy_returns.cumsum().cummax()).min()

        results.append(WFOResult(
            train_start=train_df.index[0].strftime("%Y-%m-%d"),
            train_end=train_df.index[-1].strftime("%Y-%m-%d"),
            test_start=test_df.index[0].strftime("%Y-%m-%d"),
            test_end=test_df.index[-1].strftime("%Y-%m-%d"),
            sharpe_ratio=sharpe,
            total_return=total_return,
            max_drawdown=max_dd
        ))

    return results

実運用での課題

データリーケージ

バックテストで95%の精度がライブで70%に低下するケースの多くはデータリーケージが原因。

リーケージの種類具体例対策
ルックアヘッドバイアス未来のデータを訓練に使用厳格な時系列分割
サバイバーシップバイアス上場廃止銘柄を除外全銘柄ヒストリカルデータ使用
ターゲットリーケージターゲット変数と相関する特徴量特徴量生成時点を厳密に管理
def validate_no_leakage(
    features: pd.DataFrame,
    target: pd.Series,
    prediction_horizon: int = 5
) -> bool:
    """データリーケージをチェック"""

    # 特徴量の最新日付がターゲット算出前であることを確認
    for col in features.columns:
        if features[col].shift(-prediction_horizon).corr(target) > 0.9:
            print(f"警告: {col} にリーケージの可能性")
            return False

    return True

レジーム変化

市場環境の変化(インフレシフト、金利上昇等)でモデル精度が25%低下するケースがある。

def detect_regime_change(returns: pd.Series, window: int = 60) -> pd.Series:
    """レジーム変化を検出"""

    rolling_vol = returns.rolling(window).std() * np.sqrt(252)
    rolling_mean = returns.rolling(window).mean() * 252

    # ボラティリティの急変をレジーム変化として検出
    vol_zscore = (rolling_vol - rolling_vol.mean()) / rolling_vol.std()

    regime_change = abs(vol_zscore) > 2  # 2σ超でレジーム変化

    return regime_change

対策まとめ

課題影響対策
データリーケージ精度95%→70%に低下時系列分割の厳格化
レジーム変化ドローダウン30%増定期的なモデル再訓練
オーバーフィットライブで損失20%ウォークフォワード検証
計算遅延スリッページ5-10%軽量モデル・非同期推論

バックテスト結果比較

2020-2025年の日経225構成銘柄でのバックテスト結果:

モデル年率リターンSharpe Ratio最大ドローダウン勝率
LSTM単独12%1.2-18%55%
XGBoost単独10%1.1-15%54%
DQN8%0.9-20%52%
アンサンブル15%1.5-12%58%
ベンチマーク(日経225)8%0.6-22%-

主要プレイヤー比較

企業主要モデル運用資産(億USD)年平均リターン日本市場関連
Renaissance TechnologiesXGBoost + RL1,65039%
Two SigmaLSTM + CNN60015%
CitadelLightGBM + PPO58022%
SBIホールディングスLSTM + XGBoost20010%
GMOフィナンシャルHDDQN1508%

出典: Bloomberg, 各社IR資料 (2025-2026)


実装チェックリスト

本番運用前に確認すべき項目:

  • データリーケージテスト実施
  • ウォークフォワード検証でSharpe Ratio 1.0以上
  • レジーム変化検出ロジック実装
  • 取引コスト(往復0.2%以上)を含めたバックテスト
  • 小ロットでのペーパートレーディング(最低3ヶ月)
  • ストップロス・ポジションサイジングルール策定
  • モデル再訓練スケジュール確立(月次推奨)

まとめ

機械学習モデルを活用した株価予測は、適切な特徴量設計とウォークフォワード検証により、ベンチマークを上回るパフォーマンスを達成可能。ただし、データリーケージとレジーム変化への対策が不可欠。

推奨アプローチ:

  1. XGBoostで基本モデルを構築(解釈性が高い)
  2. LSTMで時系列依存性を捕捉
  3. アンサンブルで精度向上
  4. ウォークフォワード検証で過学習を防止
  5. 小ロットで実運用開始、段階的にスケールアップ

免責事項

本記事は情報提供を目的としたものであり、特定の金融商品の売買を推奨するものではありません。投資判断は読者ご自身の責任において行ってください。機械学習モデルの予測精度は保証されるものではなく、過去のパフォーマンスは将来の結果を示すものではありません。