scikit-learnで日本製造株の翌日騰落を機械学習で予測してみたら、思ったより難しかった話

AI×自動売買

テストデータでAUC 0.87、本番で勝率48%だった話

少し前にトヨタ(7203)の翌日騰落を機械学習で予測しよう、と意気込んでLightGBMを回してみました。テストデータでAUC 0.87と表示されて「これは勝ったな」と一瞬思いましたが。。。1ヶ月実運用してみると勝率48%。手数料込みでむしろマイナスです笑。どこで間違えたのか、復習も兼ねて書いていきます。

なぜ僕が機械学習に手を出したか

僕は日本の製造メーカー株が好きで、トヨタ・日立・信越化学・キーエンスあたりを中心にウォッチしています。これらは流動性が高く、決算ニュースに対する反応も比較的素直で「特徴量から翌日のリターン方向くらいは予測できるんじゃないか?」と思ったのがきっかけです。実際には、素人がpandasで思いついた特徴量を機械学習に放り込むだけだとリーク(未来情報の混入)で簡単に高スコアが出てしまうことを身をもって学びました。

特徴量設計:シンプルに、未来を覗かないように

反省点をふまえて、今回は次の3点を必ず守るようにしました。
→ 当日終値を含む特徴量は、必ず「同じ日のシグナル」のみに使う(翌日のリターン予測に翌日のデータを混ぜない)
→ 標準化は学習データのみで fit する
→ 評価は時系列分割(過去で学習→未来で検証)にする

Pythonコード:LightGBMで翌日方向を予測

製造メーカー株を3銘柄まとめて学習させ、翌営業日のClose→Close方向を予測します。yfinanceで十分な範囲なのでサンプルとしてはこれで。

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 roc_auc_score, accuracy_score

TICKERS = ["7203.T", "6501.T", "4063.T"]  # トヨタ, 日立, 信越化学

def make_features(df: pd.DataFrame) -> pd.DataFrame:
    out = pd.DataFrame(index=df.index)
    out["ret1"] = df["Close"].pct_change(1)
    out["ret5"] = df["Close"].pct_change(5)
    out["ret20"] = df["Close"].pct_change(20)
    out["vol20"] = out["ret1"].rolling(20).std()
    out["vol_ratio"] = df["Volume"] / df["Volume"].rolling(20).mean()
    out["target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)
    return out.dropna()

frames = []
for t in TICKERS:
    d = yf.download(t, period="5y", progress=False)
    f = make_features(d)
    f["ticker"] = t
    frames.append(f)

data = pd.concat(frames).sort_index()
X = data.drop(columns=["target", "ticker"])
y = data["target"]

tscv = TimeSeriesSplit(n_splits=5)
aucs, accs = [], []
for tr, te in tscv.split(X):
    model = lgb.LGBMClassifier(
        n_estimators=300, learning_rate=0.05,
        num_leaves=31, subsample=0.8, colsample_bytree=0.8,
        random_state=42,
    )
    model.fit(X.iloc[tr], y.iloc[tr])
    proba = model.predict_proba(X.iloc[te])[:, 1]
    aucs.append(roc_auc_score(y.iloc[te], proba))
    accs.append(accuracy_score(y.iloc[te], (proba > 0.5).astype(int)))

print("AUC mean:", np.mean(aucs))
print("Accuracy mean:", np.mean(accs))

「AUC 0.55あれば御の字」と思ったほうがいい

このやり方で僕の手元では、AUCがだいたい0.53〜0.56、Accuracyが0.52〜0.54の間で揺れます。最初は「あれ、AUC 0.55ってしょぼくないか?」と思ったんですが、株価の翌日方向を素朴な特徴量で予測するなら、これくらいで十分まともと考えるべきだと最近は思っています。むしろ、AUC 0.8みたいな数字が出たら、ほぼ確実にどこかでリークしていると疑ってかかるべきです。

まとめ:勝てるモデルより「裏切られないモデル」を作る

個人的には、機械学習トレードは「派手に勝てるモデル」より「期待値どおりに動いてくれる地味なモデル」を作るほうが、家計と精神衛生の両面でずっと大事だと感じました。次は同じ枠組みで、決算発表日の前後を除外したサブ戦略でどうなるかを検証してみる予定です。同じように機械学習でドツボにハマっている方は、まず時系列分割とリークチェックから見直してみてください。

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