バックテストとフォワードテストの重要性
FXでも株式市場でも同じですが、テクニカル分析+システムトレードの大きな強みは、
「過去データを用いて、自分が考えたテクニカル指標や戦略が期待通りの成績を残せるかを事前に評価できること」にあります。
もちろん、いきなり本番取引で利益を出したいという気持ちも理解できます。
ですが、最終的に果実を得るためには 戦略を事前にしっかり評価するプロセス が何より重要です。
最近では「システムトレードは利益を出すことだけでなく、そのプロセス自体を楽しめるかどうかが大切なのではないか」と感じています。
バックテストとは?
バックテストとは、過去の株価や為替データに対して、自分が作った売買ルールやアルゴリズムを適用し、
「その戦略が実際にどのような成績を出していたか」を検証する作業です。
バックテストの目的
- 戦略が 机上の空論ではないか を確認できる
- 損益の推移や勝率、ドローダウン(資産の減少幅)を数値で把握できる
- 改善点を見つけ、戦略を磨き上げられる
勝率やドローダウンをどの指標で見るべきかは システムトレードの評価基準一覧 で整理しています。
実際のやり方例
- 過去数年分の株価や為替データを用意する
- 自分の売買ルール(例:移動平均線のゴールデンクロスで買い、デッドクロスで売り)を適用
- 各トレードの損益を集計し、勝率やリターンを算出
- グラフ化して損益曲線を確認
フォワードテストとは?
フォワードテストとは、バックテストで有効と判断された戦略を、
今度は 実際の相場の最新データ を使って検証する作業です。
いきなり実資金で取引するのではなく、デモ口座や少額のリアル資金を使ってテストします。
フォワードテストの目的
- 過去データだけでなく、現在の相場環境でも通用するか を確認できる
- サーバーの処理速度や注文遅延など、実運用特有の問題 を把握できる
- 精神的なプレッシャーを軽減しつつ、現実に近い環境で評価できる
実際のやり方例
- バックテストで有望だった戦略を選ぶ
- デモ口座で1〜3か月ほど運用し、実際の成績を記録する
- バックテストとの乖離がないか確認
- 問題がなければ少額資金で本番運用を開始
プログラムが苦手な人でもできること
ここまでシステムトレード前提で話しましたが、
プログラムに明るくない方でも Excelなどを使えば過去データを基にシミュレーション できます。
過去データは証券会社や公開API、無料サイトなどから入手しやすいため、
「もしこのルールで取引していたらどうなっていたか」を手軽に検証することが可能です。
Python によるバックテストのコード例
実際にどのようにバックテストを実装するのか、ゴールデンクロス / デッドクロス を使った最小構成のサンプルを示します。pandas と yfinance だけで動く最小例です。
import pandas as pd
import yfinance as yf
# 過去5年分の日足を取得(例: トヨタ 7203.T)
df = yf.download("7203.T", period="5y", interval="1d")
df = df.rename(columns=str.lower).dropna()
# 短期/長期の移動平均
df["sma_short"] = df["close"].rolling(25).mean()
df["sma_long"] = df["close"].rolling(75).mean()
# シグナル: 短期 > 長期 なら買い(1), そうでなければ現金(0)
df["signal"] = (df["sma_short"] > df["sma_long"]).astype(int)
# 翌日の始値で約定する前提にするため shift(1)
df["ret"] = df["close"].pct_change()
df["strategy"] = df["signal"].shift(1) * df["ret"]
# 累積リターン
df[["ret", "strategy"]].add(1).cumprod().plot()
# 成績サマリー
total_return = df["strategy"].add(1).prod() - 1
win_rate = (df["strategy"] > 0).mean()
max_dd = (df["strategy"].add(1).cumprod() /
df["strategy"].add(1).cumprod().cummax() - 1).min()
print(f"総リターン: {total_return:.2%}")
print(f"勝率 : {win_rate:.2%}")
print(f"最大DD : {max_dd:.2%}")
このコードの骨格さえ押さえておけば、あとは signal の作り方を差し替えるだけで、ボリンジャーバンドでも RSI でも同じ枠組みで検証できます。
最初にハマった落とし穴
shift(1)を入れ忘れる: その日の終値シグナルをその日の終値で約定させてしまい、未来データを覗き見した形になって成績が過剰に良くなります。- 欠損値の扱い: 祝日や上場廃止で抜けがあると、
pct_change()の結果が歪みます。dropna()の位置が重要です。 - 手数料・スプレッドの未考慮: 1 回 0.05% のコストでも、年 200 回転なら約 10% のリターン差になります。初期段階からコストを差し引いた値で評価するのが無難です。
カーブフィッティングを避ける
バックテストで一番の敵は カーブフィッティング(過剰最適化) です。パラメータをいじり倒して過去データに合わせ込むと、本番ではまったく通用しないモデルが出来上がります。
私が実際に踏んでしまった典型的なパターンはこの3つでした。
- 全期間で最適化して最高成績を採用: 直近相場に特化した設定になり、別の相場環境では逆ポジを取るようになる
- パラメータを 10 個以上同時に最適化: 組み合わせ爆発で偶然良いだけの設定が必ず出てくる
- サンプル数が少ない銘柄・期間での検証: 数十トレードしか発生せず、勝率の信頼区間が広すぎる
これを避けるには、次のルールで運用するのが効きました。
- 期間を分けて検証する(In-Sample と Out-of-Sample)
- パラメータの敏感度を見る(近傍値でも成績が大きく崩れないか)
- 最低でも 200 トレード以上が発生する期間 × 銘柄で評価する
戦略が「偶然の当たり」ではないかを統計的に判定する手順は 戦略の有効性を統計的に評価する にまとめました。
ウォークフォワード分析
カーブフィッティング対策の決定版が ウォークフォワード分析 です。過去データを時間方向にスライドさせながら、「最適化区間」と「検証区間」を交互に動かしていきます。
|---- 最適化 1 ----|-- 検証 1 --|
|---- 最適化 2 ----|-- 検証 2 --|
|---- 最適化 3 ----|-- 検証 3 --|
各「検証 N」で得られた成績だけを連結したものが、そのロジックの 擬似的な実運用成績 になります。Python で最小構成を書くと次のようになります。
import numpy as np
def walk_forward(df, train_len=252 * 2, test_len=252, param_grid=range(5, 60, 5)):
results = []
start = 0
while start + train_len + test_len <= len(df):
train = df.iloc[start : start + train_len]
test = df.iloc[start + train_len : start + train_len + test_len]
# 最適パラメータ探索(例: 短期SMAの期間)
best_param, best_score = None, -np.inf
for p in param_grid:
score = evaluate(train, sma_short=p) # 自作の評価関数
if score > best_score:
best_score, best_param = score, p
# 決まったパラメータで検証区間を走らせる
results.append(evaluate(test, sma_short=best_param))
start += test_len # 区間を1年分スライド
return np.array(results)
この結果が一貫してプラスに推移していれば、パラメータの選び方自体が一定の汎化性能を持つと言えます。逆に大きなブレが残るようなら、そのロジックはまだ本番運用に耐えません。
ウォークフォワードはパラメータ組み合わせが爆発しやすいため、実行時間が課題になります。その高速化の実例は バックテストを並列化して7日→1日に短縮した話 で紹介しています。
まとめ
- バックテストで過去のデータに対する有効性を確認
- フォワードテストで現実の市場での通用性を確認
- 両方を経ることで戦略の信頼性が高まり、安心して運用できる
- カーブフィッティングを避けるため、In-Sample / Out-of-Sample 分割とウォークフォワードを基本にする
システムトレードは「利益を出すこと」だけでなく、
戦略を組み立て、検証し、改善していくプロセス自体を楽しむこと が継続の秘訣だと思います。