最終更新: 2026年06月
機械学習モデルをFXに適用したバックテストで「良い結果が出た」と喜んでいたが、実運用では全然機能しなかった——この経験を持つエンジニア・トレーダーは多い。多くの場合、その原因は「データリーク」か「正しくない検証設計」だ。バックテストの問題は統計学的に深く、特に時系列データにおける検証設計は機械学習の教科書そのままでは対応できない部分がある。本稿では、Python実装の具体的なコードとともに、正しい機械学習FXバックテストの設計方法を解説する。
なぜ機械学習バックテストは失敗しやすいのか
通常の機械学習プロジェクトでは、データをランダムにシャッフルして訓練・検証・テストセットに分割することが一般的だ。しかし金融時系列データでこれをやると、「未来の情報が訓練データに混入する」というデータリークが発生する。
例えば2024年3月のレートデータを使って2024年1月のトレードシグナルを予測させる——これは現実には不可能だが、ランダム分割をするとこのような状況が生まれる。バックテスト上のパフォーマンスは良く見えるが、実運用では当然機能しない。
もう一つの罠が「擬似ラベル問題」だ。「翌日上がったら1、下がったら0」というラベルを作る際に、未来の価格が関与するため、訓練データに未来情報が含まれてしまう。
正しい時系列データの分割設計
基本原則:時間的順序を絶対に保持する
import pandas as pd
import numpy as np
# データ読み込み(時系列順)
df = pd.read_csv('usdjpy_daily.csv', parse_dates=['date'], index_col='date')
df = df.sort_index() # 時間順にソート
# 正しい分割(時間順を保持)
total_len = len(df)
train_end = int(total_len * 0.70)
val_end = int(total_len * 0.85)
train_df = df.iloc[:train_end] # 訓練: 全体の70%
val_df = df.iloc[train_end:val_end] # 検証: 全体の15%
test_df = df.iloc[val_end:] # テスト: 全体の15%(未来データ)
# NG: ランダム分割(時系列ではこれをやってはいけない)
# from sklearn.model_selection import train_test_split
# X_train, X_test = train_test_split(X, test_size=0.3, shuffle=True) # 絶対禁止
Qiitaの実装事例では、36ヶ月分のデータを30ヶ月の訓練・3ヶ月の検証・3ヶ月のバックテストと分割するアプローチが提案されている。
ウォークフォワード検証(時系列クロスバリデーション)
単純な前後分割より堅牢な方法が、ウォークフォワード交差検証だ。
from sklearn.model_selection import TimeSeriesSplit
# 時系列専用のクロスバリデーション
tscv = TimeSeriesSplit(n_splits=5, gap=20) # gap: 訓練と検証の間に設けるバッファ
for fold, (train_idx, test_idx) in enumerate(tscv.split(X)):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# モデル訓練
model.fit(X_train, y_train)
# 各フォールドでの評価
score = model.score(X_test, y_test)
print(f"Fold {fold+1}: 方向精度 = {score:.4f}")
gapパラメーターは重要だ。訓練データの末尾と検証データの先頭の間にバッファを設けることで、特徴量計算に使う移動平均等のルックアヘッドバイアスを防ぐ。
特徴量エンジニアリング:未来情報を使わない設計
def create_features(df, lookback=20):
"""過去データのみを使った特徴量生成"""
features = pd.DataFrame(index=df.index)
# 価格変化率(過去のみ参照)
features['return_1d'] = df['close'].pct_change(1)
features['return_5d'] = df['close'].pct_change(5)
features['return_20d'] = df['close'].pct_change(20)
# ボラティリティ(過去20日)
features['volatility_20d'] = df['close'].pct_change().rolling(20).std()
# RSI(過去データのみ使用)
def rsi(prices, period=14):
delta = prices.diff()
gain = delta.where(delta > 0, 0).rolling(period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
features['rsi_14'] = rsi(df['close'])
# 移動平均乖離率
ma20 = df['close'].rolling(20).mean()
features['ma20_deviation'] = (df['close'] - ma20) / ma20
# ラベル生成(注意: 未来の価格を使うが、訓練時の扱いに注意)
features['label'] = (df['close'].shift(-1) > df['close']).astype(int)
return features.dropna()
features_df = create_features(df)
ラベル(label)に未来の価格(shift(-1))を使うのは避けられない。しかし重要なのは、このラベルは「正解データ」として扱い、特徴量には絶対に含めないことだ。
Backtesting.pyを使ったバックテスト実装
機械学習のシグナルをバックテストするためのPythonライブラリとして、Backtesting.pyは学習コストが低く実用的だ。
pip install backtesting scikit-learn xgboost
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
import numpy as np
class MLStrategy(Strategy):
# クラス変数として機械学習モデルを渡す
model = None
lookback = 20
def init(self):
# 特徴量を事前に計算しておく
pass
def next(self):
# next()内では「現在以前のデータのみ」参照できる
if len(self.data.Close) < self.lookback + 5:
return
# 直近データで特徴量を計算
closes = pd.Series(self.data.Close[-self.lookback:])
returns = closes.pct_change().dropna()
# 特徴量ベクトル(単純化した例)
features = np.array([
returns.mean(), # 平均リターン
returns.std(), # ボラティリティ
returns.iloc[-1], # 直近リターン
]).reshape(1, -1)
# モデルで予測
if self.model is not None:
prediction = self.model.predict(features)[0]
if prediction == 1 and not self.position:
self.buy()
elif prediction == 0 and self.position:
self.position.close()
# モデルの訓練(テストデータは渡さない)
# ... (特徴量生成・訓練コードを挿入)
# バックテスト実行
MLStrategy.model = trained_model
bt = Backtest(test_df, MLStrategy,
cash=1_000_000,
commission=0.0002, # スプレッドを考慮
trade_on_close=False)
result = bt.run()
print(result)
Backtesting.pyのnext()メソッドは設計上、「現時点以前のデータのみ参照可能」という制約があり、未来データへのアクセスがアーキテクチャレベルで防がれている。
Claudeと会話しながらインジケータが作れるhedgrow-fxはこちら。機械学習のシグナル設計をClaude対話で洗練させるアプローチも有効だ。
主要バックテストライブラリの比較
| ライブラリ | 学習コスト | 機械学習連携 | 処理速度 | 向いているケース | |---|---|---|---|---| | Backtesting.py | 低 | 中(自分で実装) | 中 | プロトタイプ・学習用 | | Backtrader | 中 | 中 | 中 | 複雑な戦略・複数通貨ペア | | vectorbt | 高 | 高 | 高速 | 大量パターン探索・機械学習統合 |
vectorbtは行列演算ベースで非常に高速な反面、学習コストが高い。まずはBacktesting.pyで基本を習得してから移行するのが現実的だ。
バックテストで見るべき指標
機械学習バックテストの評価では、以下の指標を総合的に確認する。
| 指標 | 最低ライン目安 | 説明 | |---|---|---| | 勝率(Win Rate) | 50%以上(必須ではない) | 1回あたりのトレード勝率 | | プロフィットファクター | 1.5以上 | 総利益 ÷ 総損失 | | シャープレシオ | 1.0以上 | リスク調整後リターン | | 最大ドローダウン | 20%以下 | 許容ライン以内か | | 方向予測精度 | 55%以上(連続性も確認) | ランダム(50%)との差分 |
重要: これらの指標は全てテストセット(訓練に使わなかった未来データ)で計算すること。訓練データ上での成績は参考にならない。
データリーク チェックリスト
機械学習FXバックテスト実装前に確認すべき項目:
- [ ] 特徴量計算に
shift(-1)など未来参照が混入していないか - [ ] 訓練/検証/テスト分割がランダムシャッフルになっていないか
- [ ] 正規化のscalerを訓練データだけでfit_transformしているか(テストはtransformのみ)
- [ ] 移動平均等のローリング計算で
min_periodsが適切か - [ ] バックテストライブラリ内でのルックアヘッドが発生していないか
まとめ
機械学習をFXバックテストに活用する際、最大の敵は「データリーク」と「過学習」だ。正しい時系列分割・ウォークフォワード検証・未来参照の排除という設計原則を守ることが、実用的なシステム構築の前提条件になる。
どれだけ優れた機械学習モデルを作っても、バックテスト設計が間違っていれば「見せかけの好成績」しか得られない。実運用前には必ず「時間的に未来のデータ」でのバックテスト結果を確認すること。
FXには損失リスクがある。機械学習モデルのバックテスト上の成績が実際の運用成績と一致するとは限らない。十分なフォワードテスト期間を設けた上での慎重な運用が不可欠だ。
よくある質問(FAQ)
Q: 機械学習FXバックテストで最も多いミスは何ですか?
A: データリーク(未来情報の混入)だ。特に特徴量計算でのshift(-1)の使用誤り、ランダムなデータ分割、scalerをテストデータでfit_transformすることが頻出ミスだ。
Q: ランダムフォレストとLSTMではどちらがFX予測に向いていますか? A: 一般論として、ランダムフォレストは実装が簡単で過学習耐性が高い。LSTMは時系列の長期依存関係を学習できる可能性がある。特定の通貨ペア・時間足での比較実験が最善だ。
Q: バックテストで良い結果が出ない場合はどうすればよいですか? A: まず「データリークがないか」を確認する。次に特徴量を見直す。それでも改善しない場合、「その期間・通貨ペアで機械学習が有効なシグナルを見つけられなかった」という結論も正直な結果だ。
Q: 手数料・スリッページはバックテストに含める必要がありますか?
A: 必ず含めること。Backtesting.pyではcommissionパラメーターで設定できる。スプレッド込みで計算しないと実運用で大きく乖離する。
Q: Pythonで作った機械学習モデルをMT4/MT5と連携させる方法はありますか? A: Python-MT4/MT5間のソケット通信が一般的なアプローチだ。ZeroMQやDDEサーバーを使う実装例がコミュニティにある。Claudeと会話しながらインジケータが作れるhedgrow-fxはこちら。
