PythonでFX自動売買のバックテストをやる方法 — Backtesting.pyで戦略を検証する
はじめに — Bot を本番に投入する前に、必ずやるべきこと
FX自動売買Botを開発したとき、「とりあえず動くようになったから実口座で回してみよう」と考える人は多い。しかし、バックテストなしでBotを本番に投入するのは、地図なしで登山をするようなものです。
バックテストとは、過去の価格データを使って戦略を事前にシミュレーションすること。実際のお金を使う前に、その戦略が過去の相場でどう振る舞ったかを検証できます。
この記事では、Pythonの Backtesting.py ライブラリを使って、FX戦略のバックテストをゼロから実装する手順を解説します。移動平均クロスというシンプルな戦略を題材に、データ取得から結果の読み方、そしてバックテストの落とし穴まで網羅します。
FX自動売買の全体像を先に把握したい方は FX自動売買の始め方 完全ガイド をどうぞ。
バックテストとは何か
バックテストとは、売買ルール(戦略)を過去の価格データに適用し、仮にその戦略で取引していたらどういう結果になったかをシミュレーションする手法です。
なぜBot実稼働前に必須なのか
バックテストが必要な理由は3つあります。
- 致命的なバグの発見: 損切りロジックが機能していない、ポジション方向が逆、といった致命的な実装ミスをお金を失う前に発見できる
- 戦略の足切り: バックテストで利益が出ない戦略は、本番でも利益が出ない可能性が極めて高い。明らかにダメな戦略を事前に除外できる
- パラメータの基準値把握: 移動平均の期間、利確/損切り幅など、パラメータの妥当な範囲を事前に把握できる
ただし注意点があります。バックテストで利益が出たからといって、本番でも利益が出るとは限りません。バックテストは「最低限の足切り基準」として使うもの。この点については記事の後半で詳しく解説します。
バックテスト結果の評価指標について詳しくは FX自動売買の成績評価 — 勝率・シャープレシオ・最大DDの見方 を参照してください。
Backtesting.py のインストールと基本構造
ライブラリの選定理由
Python でバックテストを行うライブラリはいくつかありますが、この記事では Backtesting.py を使います。
選定理由:
- コードがシンプル: 戦略を1つのクラスで定義でき、学習コストが低い
- 可視化が標準装備:
bt.plot()で損益曲線やエントリー/イグジットポイントをグラフ化できる - 軽量: 依存ライブラリが少なく、環境構築が楽
インストール
pip install backtesting
pandas と numpy も使うので、未インストールなら合わせて入れます。
pip install pandas numpy
基本構造
Backtesting.py の基本的な使い方は以下の3ステップです。
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
import pandas as pd
# 1. 戦略クラスを定義
class MyStrategy(Strategy):
def init(self):
# インジケーターの初期化
pass
def next(self):
# 各バーで呼ばれる売買ロジック
pass
# 2. ヒストリカルデータを読み込み(pandas DataFrame)
data = pd.read_csv("ohlcv.csv", parse_dates=True, index_col="Date")
# 3. バックテスト実行
bt = Backtest(data, MyStrategy, cash=100000, commission=0.0004)
stats = bt.run()
print(stats)
bt.plot()
ポイントは Strategy クラス の2つのメソッドです。
| メソッド | 呼ばれるタイミング | 役割 |
|---|---|---|
init() |
バックテスト開始時に1回 | インジケーター(移動平均など)の事前計算 |
next() |
各バー(ローソク足)ごとに1回 | 売買判断とポジション操作 |
next() メソッド内で self.buy() や self.sell() を呼ぶことで、仮想的な売買が実行されます。
ヒストリカルデータの取得方法
バックテストには 過去の価格データ(OHLCV) が必要です。OHLCVとは Open(始値)、High(高値)、Low(安値)、Close(終値)、Volume(出来高)の5つの値を持つ時系列データです。
方法1: GMOコイン APIから取得
GMOコインは暗号資産取引所ですが、Public APIでヒストリカルデータを無料で取得できます。認証不要のAPIなので、口座がなくても利用可能です。
import requests
import pandas as pd
from datetime import datetime, timedelta
def fetch_gmo_klines(symbol="BTC_JPY", interval="1hour", days=30):
"""GMOコインAPIからOHLCVデータを取得する"""
base_url = "https://api.coin.z.com/public/v1/klines"
all_data = []
# 日付を1日ずつ指定して取得
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
current = start_date
while current <= end_date:
date_str = current.strftime("%Y%m%d")
params = {
"symbol": symbol,
"interval": interval,
"date": date_str,
}
response = requests.get(base_url, params=params)
data = response.json()
if data["status"] == 0 and data.get("data"):
for candle in data["data"]:
all_data.append({
"Date": pd.to_datetime(candle["openTime"], unit="ms"),
"Open": float(candle["open"]),
"High": float(candle["high"]),
"Low": float(candle["low"]),
"Close": float(candle["close"]),
"Volume": float(candle["volume"]),
})
current += timedelta(days=1)
df = pd.DataFrame(all_data)
df.set_index("Date", inplace=True)
df.sort_index(inplace=True)
return df
# 使用例: BTC/JPYの1時間足を30日分取得
data = fetch_gmo_klines(symbol="BTC_JPY", interval="1hour", days=30)
print(f"取得件数: {len(data)}")
print(data.head())
interval に指定できる値: 1min, 5min, 10min, 15min, 30min, 1hour, 4hour, 8hour, 12hour, 1day, 1week, 1month
方法2: CSVファイルから読み込み
すでにCSVファイルでデータを持っている場合は、pandasで直接読み込めます。
import pandas as pd
# CSV形式: Date, Open, High, Low, Close, Volume
data = pd.read_csv(
"btc_jpy_1h.csv",
parse_dates=["Date"],
index_col="Date",
)
# Backtesting.py が要求するカラム名に合わせる
data.columns = ["Open", "High", "Low", "Close", "Volume"]
print(data.head())
Backtesting.py が要求するDataFrame形式:
- インデックス: DatetimeIndex(日時)
- カラム名:
Open,High,Low,Close,Volume(先頭大文字)
カラム名が小文字の場合はエラーになるので注意してください。
方法3: yfinance で為替データを取得
FX通貨ペアのデータが必要な場合、yfinanceから取得する方法もあります。
pip install yfinance
import yfinance as yf
# USD/JPYの日足を1年分取得
data = yf.download("USDJPY=X", start="2025-01-01", end="2026-01-01")
# Backtesting.pyの形式に合わせる
data = data[["Open", "High", "Low", "Close", "Volume"]]
data.columns = ["Open", "High", "Low", "Close", "Volume"]
print(data.head())
ただし yfinance のFXデータは日足のみで、分足・時間足には対応していない場合があります。短い時間足でバックテストしたい場合はGMOコインAPIやCSVを使ってください。
移動平均クロス戦略のバックテスト実装
ここから実際にバックテストを実装します。題材は 移動平均クロス — 最もシンプルかつ広く知られたトレンドフォロー戦略です。
戦略のルール
- 買いエントリー: 短期移動平均(SMA10)が長期移動平均(SMA30)を 上抜け したら買い
- 売りエグジット: 短期移動平均が長期移動平均を 下抜け したら決済
- 売りエントリー: 今回は買いのみ(ロングオンリー)で実装
完全な実装コード
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA
import pandas as pd
class SmaCross(Strategy):
"""移動平均クロス戦略"""
# パラメータ(最適化可能)
n1 = 10 # 短期SMA期間
n2 = 30 # 長期SMA期間
def init(self):
# SMAインジケーターを事前計算
self.sma_short = self.I(SMA, self.data.Close, self.n1)
self.sma_long = self.I(SMA, self.data.Close, self.n2)
def next(self):
# 短期SMAが長期SMAを上抜け → 買い
if crossover(self.sma_short, self.sma_long):
self.buy()
# 短期SMAが長期SMAを下抜け → 決済
elif crossover(self.sma_long, self.sma_short):
self.position.close()
# ヒストリカルデータの読み込み
data = pd.read_csv("btc_jpy_1h.csv", parse_dates=["Date"], index_col="Date")
# バックテスト実行
bt = Backtest(
data,
SmaCross,
cash=100_000, # 初期資金: 10万円
commission=0.0004, # 手数料: 0.04%
exclusive_orders=True,
)
stats = bt.run()
print(stats)
コードの解説
self.I() メソッド
self.I(func, *args) はインジケーターを登録するメソッドです。init() 内でこのメソッドを使ってインジケーターを事前計算しておくと、next() 内で最新の値にアクセスできます。
crossover() 関数
crossover(series_a, series_b) は、series_a が series_b を下から上に抜けた瞬間に True を返します。移動平均のゴールデンクロス(買いシグナル)やデッドクロス(売りシグナル)の判定に使います。
exclusive_orders=True
既存のポジションと反対方向の注文が入ったとき、既存ポジションを先に決済してから新しい注文を出す設定です。
パラメータ最適化
Backtesting.py には、パラメータを自動で最適化する機能があります。
stats, heatmap = bt.optimize(
n1=range(5, 30, 5), # 短期SMA: 5, 10, 15, 20, 25
n2=range(20, 80, 10), # 長期SMA: 20, 30, 40, 50, 60, 70
maximize="Equity Final [$]",
return_heatmap=True,
)
print(f"最適パラメータ: n1={stats._strategy.n1}, n2={stats._strategy.n2}")
print(f"最終資産: {stats['Equity Final [$]']:,.0f}円")
ただし、パラメータ最適化は諸刃の剣です。この点は後半の「バックテストの落とし穴」で詳しく解説します。
結果の読み方 — 5つの主要指標
bt.run() の戻り値には多数の統計値が含まれます。特に重要な5つの指標を解説します。
1. 勝率(Win Rate)
Win Rate [%] 52.3
勝ちトレード数 / 全トレード数 の割合。勝率が高ければ良い戦略とは限りません。勝率40%でも、1回の利益が損失の3倍なら十分に利益が出ます。勝率は単独ではなく、プロフィットファクターと合わせて評価してください。
2. プロフィットファクター(PF)
Profit Factor 1.45
総利益 / 総損失 の比率。1.0を超えれば利益がプラス、1.5以上あれば実用的な水準とされます。
| PF | 評価 |
|---|---|
| 1.0未満 | 赤字。戦略を根本から見直す |
| 1.0〜1.3 | 微益。手数料・スリッページを加味すると赤字の可能性 |
| 1.3〜2.0 | 実用的。ただしバックテストの数値なので3割引きで見積もる |
| 2.0以上 | 高い。過剰最適化の疑いを確認すべき |
3. 最大ドローダウン(Max Drawdown)
Max. Drawdown [%] -15.2
資金のピークからの最大下落率。この数字が自分の許容範囲を超えていたら、その戦略は使えません。
一般的な目安:
- 10%以下: 保守的で安定した戦略
- 10〜20%: 許容範囲(ただしメンタルが試される)
- 20%以上: リスクが高い。パラメータ調整またはポジションサイズの縮小を検討
最大DDの詳しい解説と管理方法は FX Bot のドローダウン管理と自動停止の仕組み を参照してください。
4. シャープレシオ(Sharpe Ratio)
Sharpe Ratio 0.87
リスク(リターンの振れ幅)に対するリターンの効率性を示す指標。値が高いほど、取ったリスクに対して効率的にリターンを得ていることを意味します。
| シャープレシオ | 評価 |
|---|---|
| 0.5未満 | 低い。リスクに見合うリターンが出ていない |
| 0.5〜1.0 | まずまず。改善の余地あり |
| 1.0〜2.0 | 良好 |
| 2.0以上 | 非常に高い。過剰最適化の可能性を疑う |
5. 総取引回数(# Trades)
# Trades 48
意外と見落としがちですが、取引回数が少なすぎると統計的な信頼性が低い。最低でも30回以上の取引があるデータで評価してください。10回の取引で勝率80%でも、それは偶然かもしれません。
結果の出力例
stats = bt.run()
print(stats)
# 出力例:
# Start 2025-06-01 00:00:00
# End 2026-05-01 00:00:00
# Duration 335 days 00:00:00
# Equity Final [$] 112,450
# Equity Peak [$] 118,200
# Return [%] 12.45
# Buy & Hold Return [%] 8.32
# Max. Drawdown [%] -15.2
# Avg. Drawdown [%] -4.1
# # Trades 48
# Win Rate [%] 52.3
# Profit Factor 1.45
# Sharpe Ratio 0.87
# SQN 1.12
Return [%] が Buy & Hold Return [%](単純に買って持ち続けた場合のリターン)を上回っているかも重要なチェックポイントです。Buy & Hold に負けている戦略は、わざわざBotを動かす意味がありません。
グラフで視覚的に確認
bt.plot()
このメソッドを呼ぶと、ブラウザ上にインタラクティブなチャートが表示されます。
表示される情報:
- 価格チャートとインジケーター(SMAライン)
- エントリー/イグジットのマーカー
- 損益曲線(Equity Curve)
- ドローダウンの推移
損益曲線が右肩上がりでなく、特定の期間だけで大きく稼いでいる場合は、その期間の相場環境にたまたま合っていただけの可能性があります。
バックテストの落とし穴 — 過信は禁物
バックテストは強力なツールですが、構造的な限界 を理解せずに使うと、かえって危険です。
落とし穴1: カーブフィッティング(過剰最適化)
最も多い失敗が カーブフィッティング。過去のデータに合わせすぎたパラメータは、未来の相場では機能しません。
× ダメな例:
2025年のデータで最適化 → SMA(7, 23) が最も利益が出た
→ そのまま2026年に適用 → 連敗
○ 良い例:
2024年のデータで最適化 → SMA(5〜15, 20〜40) の広い範囲で利益が出る
→ 2025年のデータで検証(ウォークフォワード分析)
→ 2025年でも利益が出ることを確認してから採用
対策: ウォークフォワード分析
データを「最適化期間」と「検証期間」に分けます。
# 全期間のデータを分割
train_data = data[:"2025-12-31"] # 最適化期間
test_data = data["2026-01-01":] # 検証期間
# 最適化期間でパラメータ探索
bt_train = Backtest(train_data, SmaCross, cash=100_000, commission=0.0004)
stats_train = bt_train.optimize(
n1=range(5, 30, 5),
n2=range(20, 80, 10),
maximize="Sharpe Ratio",
)
# 見つけたパラメータで検証期間をテスト
bt_test = Backtest(test_data, SmaCross, cash=100_000, commission=0.0004)
stats_test = bt_test.run(
n1=stats_train._strategy.n1,
n2=stats_train._strategy.n2,
)
print("=== 検証期間の結果 ===")
print(stats_test)
検証期間でも利益が出ているか、最大DDが許容範囲内かを確認します。
落とし穴2: スリッページの無視
バックテストでは「その瞬間のClose値で約定した」と仮定しますが、実際の取引では 注文を出してから約定するまでに価格がずれる(スリッページ)ことがあります。
特に以下の状況でスリッページが大きくなります:
- 指標発表直後の急激な値動き
- 流動性が低い時間帯(早朝・年末年始)
- 大きなロットでの注文
Backtesting.py では commission パラメータでスリッページ分も含めた手数料を設定できます。実際のスリッページを測定できない段階では、余裕を持って0.05〜0.1%程度を上乗せ しておくのが安全です。
# commission にスリッページ分を含めて設定
bt = Backtest(data, SmaCross, cash=100_000, commission=0.001) # 0.1%
落とし穴3: 生存者バイアス
ネット上で「この戦略で月利10%」という報告を見かけますが、それは うまくいった人だけが発信しているから。同じ戦略で損した人は黙っています。
自分のバックテスト結果にも同じバイアスがかかります。10種類の戦略をバックテストして、利益が出た1つだけを「見つけた!」と採用するのは、事実上ランダムに選んでいるのと変わりません。
対策:
- 戦略には理論的な根拠(なぜこの戦略が機能するのか)を持つ
- 異なる市場環境(トレンド相場・レンジ相場)でテストする
- 複数の通貨ペア・時間足でテストして頑健性を確認する
落とし穴4: スプレッド変動の無視
バックテストではスプレッドを固定値で計算しますが、実際のスプレッドは時間帯やボラティリティによって変動します。特に 指標発表時のスプレッド拡大 はバックテストでは再現が難しい。
通常時のスプレッドが0.2銭の通貨ペアでも、米雇用統計発表時には10銭以上に広がることがあります。バックテストで「指標発表の直後にエントリーして利益を出している」パターンがあったら、実際にはスプレッド負けしている可能性を疑ってください。
落とし穴5: データ期間の偏り
1年分のデータだけでバックテストした場合、その1年がたまたまトレンド相場だったら、トレンドフォロー戦略は良い成績を出します。しかし翌年がレンジ相場だったら、同じ戦略が連敗する可能性があります。
対策:
- 最低でも2〜3年分のデータでテストする
- トレンド期間とレンジ期間の両方を含むデータを使う
- 年ごと・四半期ごとの成績を分けて確認する
バックテストから実口座へ — 段階的に進める
バックテストで合格基準を満たした戦略が見つかったら、次のステップに進みます。ただし、バックテストから一気に本番運用に飛ぶのは危険です。
バックテスト合格基準の目安
以下の基準を全て満たすことを推奨します。
| 指標 | 合格基準 |
|---|---|
| プロフィットファクター | 1.3以上(理想は1.5以上) |
| 最大ドローダウン | 20%以下(理想は10%以下) |
| シャープレシオ | 0.5以上 |
| 取引回数 | 30回以上 |
| ウォークフォワード結果 | 検証期間でもプラス |
| Buy & Hold比較 | Buy & Holdを上回っている |
実口座での段階的アプローチ
- デモまたは最小ロット: まず最小取引単位で、バックテスト結果と実際の成績の乖離を確認する
- 2〜4週間のモニタリング: 実際のスリッページ、スプレッド変動、約定速度を記録する
- ロットの段階的引き上げ: 問題なければロットを少しずつ増やす
段階的なスケールアップの詳しいステップは FX自動売買 入門前に知っておくべきリスク で解説しています。
口座開設 — Bot対応のAPI環境を選ぶ
バックテストで検証した戦略を実口座で動かすには、APIに対応した口座が必要です。
自作BotでAPIトレードを行うなら、REST APIとWebSocketに対応しており、日本語ドキュメントが充実している口座が適しています。口座開設は無料で、入出金手数料もかかりません。
- REST API: シンプルな設計で学習コストが低い
- WebSocket: リアルタイムレート配信に対応
- ヒストリカルデータ API: バックテスト用の価格データも取得可能(この記事で使ったコードがそのまま使える)
- 口座開設: 最短当日
バックテストのデータ取得から本番運用まで、同じAPIで一貫して開発できるのが利点です。
Botの具体的な実装方法は PythonでFX自動売買Botを作る入門ガイド で解説しています。
まとめ — バックテストは「地図」であって「保証」ではない
この記事で解説した内容を振り返ります。
- バックテストはBot実稼働前の必須工程 — バグ発見、戦略の足切り、パラメータ把握に使う
- Backtesting.py で手軽に実装可能 — Strategy クラスを定義して
bt.run()するだけ - ヒストリカルデータは複数の方法で取得可能 — GMOコインAPI、CSV、yfinance
- 結果は5つの指標で評価 — 勝率、PF、最大DD、シャープレシオ、取引回数
- 落とし穴を理解して使う — カーブフィッティング、スリッページ、生存者バイアスに注意
バックテストは登山の「地図」のようなもの。地図を見ずに山に入るのは無謀ですが、地図があっても天候は変わる。地図で経路を確認した上で、実際の天候(相場環境)を見ながら判断していくのが、自動売買の現実的な運用スタイルです。
関連記事
- FX自動売買の始め方 完全ガイド →
- PythonでFX自動売買Botを作る入門ガイド →
- FX自動売買の成績評価 — 勝率・シャープレシオ・最大DDの見方 →
- FX Bot のドローダウン管理と自動停止の仕組み →
- FX自動売買 入門前に知っておくべきリスク →
※ 当記事のリンクにはアフィリエイトリンクを含む場合があります。口座開設の費用には影響しません。