以前LightGBMで決算プレイのバックテストをやったんですが、あれは「決算発表前後」という特定のイベントに絞った話でした。今度は「普通に明日上がるか下がるか」を機械学習で予測できないか試してみたくなりまして。ランダムフォレストは「過学習しにくい」「チューニングが少なめでもそこそこ動く」と聞いたので、まず手始めに触ってみました。結論から言うと、ランダムに賭けるよりは少しマシ、程度の精度でした(笑)。でも「少しマシ」が積み重なると長期では意味を持つので、探求の価値はあると思っています。
なぜこのテーマを調べたか
製造メーカー銘柄(主にトヨタ・コマツ・ファナック)を長期保有しながら短期トレードも試みているんですが、「明日どっちに動くか」の感覚が全然当たらない。チャートを見てもRSIを見ても、結局ビビって買えなかったりするんですよね。なら機械に判断させてみよう、というのが動機です。感情を排除するのが自動売買の一番の目的という気がしています。
ランダムフォレストとは
ランダムフォレストは「決定木をたくさん作って多数決する」アルゴリズムです。1本の決定木は訓練データに過学習しやすいのですが、ランダムにサンプルや特徴量を変えた木を100本・200本作って平均をとることで、頑健な予測ができます。株価予測に使うときのメリットは「どの特徴量が重要か(特徴量重要度)」を簡単に確認できること。テクニカル指標のどれが効いているか、感覚的に把握しやすいです。
特徴量の設計
どの指標を入力するかが精度の大半を決めます。今回は以下を使いました。シンプルな価格系特徴量と出来高系を組み合わせています。
import yfinance as yf
import pandas as pd
import numpy as np
def compute_rsi(series, period=14):
delta = series.diff()
gain = delta.clip(lower=0).rolling(period).mean()
loss = (-delta.clip(upper=0)).rolling(period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def add_features(df):
"""特徴量を追加する"""
# リターン系
df["return_1d"] = df["Close"].pct_change(1)
df["return_3d"] = df["Close"].pct_change(3)
df["return_5d"] = df["Close"].pct_change(5)
df["return_20d"] = df["Close"].pct_change(20)
# 移動平均乖離率
df["ma5"] = df["Close"].rolling(5).mean()
df["ma25"] = df["Close"].rolling(25).mean()
df["ma75"] = df["Close"].rolling(75).mean()
df["ma_ratio_5_25"] = df["Close"] / df["ma25"] - 1
df["ma_ratio_25_75"] = df["ma25"] / df["ma75"] - 1
# RSI
df["rsi14"] = compute_rsi(df["Close"], 14)
df["rsi6"] = compute_rsi(df["Close"], 6)
# ボラティリティ
df["vol_20d"] = df["return_1d"].rolling(20).std()
# 出来高比率
df["volume_ratio"] = df["Volume"] / df["Volume"].rolling(20).mean()
# ターゲット:翌日終値が上なら1、下なら0
df["target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)
return df
# トヨタ自動車でテスト
ticker = "7203.T"
raw = yf.download(ticker, start="2018-01-01", end="2026-05-31", auto_adjust=True)
df = add_features(raw.copy())
df = df.dropna()
print(f"データ期間: {df.index[0].date()} ~ {df.index[-1].date()}")
print(f"サンプル数: {len(df)}")
print(f"上昇日の割合: {df['target'].mean():.1%}")
時系列クロスバリデーションで精度を評価する
株価データで通常のランダムKFoldを使うと「未来を使って過去を予測する」という情報漏洩が起きてしまいます。必ずTimeSeriesSplitを使ってください。これが機械学習×株価でいちばん大事な注意点です。
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import accuracy_score, classification_report
import warnings
warnings.filterwarnings("ignore")
FEATURES = [
"return_1d", "return_3d", "return_5d", "return_20d",
"ma_ratio_5_25", "ma_ratio_25_75",
"rsi14", "rsi6",
"vol_20d", "volume_ratio"
]
X = df[FEATURES]
y = df["target"]
# 時系列クロスバリデーション(5分割)
tscv = TimeSeriesSplit(n_splits=5)
model = RandomForestClassifier(
n_estimators=200,
max_depth=5, # 浅めにして過学習を抑制
min_samples_leaf=20,
random_state=42,
n_jobs=-1
)
scores = []
for fold, (train_idx, test_idx) in enumerate(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]
model.fit(X_train, y_train)
pred = model.predict(X_test)
acc = accuracy_score(y_test, pred)
scores.append(acc)
print(f" Fold {fold+1}: accuracy = {acc:.3f} (テスト期間: {X_test.index[0].date()} ~ {X_test.index[-1].date()})")
print(f"\n平均精度: {np.mean(scores):.3f} ± {np.std(scores):.3f}")
print(f"ランダム予測の精度(ベースライン): {y.mean():.3f}")
特徴量重要度を確認する
最後のFoldで学習したモデルの特徴量重要度を見てみましょう。どの指標が効いているか確認すると、戦略を考えるヒントになります。
# 最終Foldのモデルで特徴量重要度を確認
importances = pd.Series(model.feature_importances_, index=FEATURES)
importances = importances.sort_values(ascending=False)
print("\n特徴量重要度:")
for feat, imp in importances.items():
bar = "█" * int(imp * 100)
print(f" {feat:20s}: {imp:.3f} {bar}")
実際に動かすとreturn_20d(20日リターン)やvol_20d(ボラティリティ)の重要度が高く、短期的な値動き(return_1d)はあまり効かない傾向が出ました。「モメンタム」と「ボラティリティの収縮・拡大」が翌日方向性のヒントになっているようです。
実際の精度と正直な感想
トヨタで試した結果、平均精度は54〜57%あたりでした。ランダム(50%)よりは少しマシですが、「これで勝てる!」という数字では全くない。。。正直なところ、製造メーカーの株は外部要因(円安・原材料費・半導体不足など)に大きく左右されるため、テクニカル指標だけでは限界がある気がします。
ただ「55%の精度でポジションサイジングをうまくやれば長期でプラスになりうる」という考え方もあります。精度50.1%でも数を増やせば期待値はプラスになる理屈です。あくまでエッジの一つとして使う、という位置づけが正しそうです。
まとめ
ランダムフォレストで翌日騰落予測を試した結果、精度は54〜57%。過学習を防ぐにはmax_depthを浅くすること、評価には必ずTimeSeriesSplitを使うことが重要です。特徴量重要度から「20日リターン」と「ボラティリティ」が効きやすいことも確認できました。
個人的には次にやりたいのがXGBoostとの比較です。LightGBMはすでに試したので、ランダムフォレスト・XGBoost・LightGBMの3モデルを同じ条件で比べてみようと思います。→ 特徴量を増やしすぎると過学習が怖いので、特徴量選択(RFEやSHAP)も勉強しないといけなさそうです。
