Python backtesting.py FX 使い方完全ガイド|RSI戦略の実装からパラメータ最適化まで
最終更新: 2026年06月
Pythonでバックテストを実装する手段はいくつかあるが、backtesting.pyはそのなかで最もシンプルな構造を持ちながら、本番運用を意識した指標計算・最適化機能を備えるライブラリである。インストールは1行、Strategyクラスを継承するだけでバックテストが動く手軽さが特徴だ。本稿では、FXへの適用を前提に、環境構築からRSIを使った売買戦略の実装、bt.run()・bt.plot()による結果解釈、optimize()によるパラメータ探索、さらにRSI+ボリンジャーバンド複合戦略や資金管理の実装比較、Google Colabでの動作確認手順、MT4/MT5のStrategyTesterとの比較まで、実際に動くコードとともに体系的に解説する。
バックテスト結果は過去データへの適合であり、将来の利益を保証するものではない。 この前提は本稿を通じて一貫して意識してほしい。
1. backtesting.py の特徴とbacktraderとの比較
1-1. backtesting.py の設計思想
backtesting.pyは2019年に公開されたPure-Pythonのバックテストフレームワークである。依存ライブラリが少なく(pandas・numpy・bokeh のみ)、インタラクティブなHTMLチャートを標準出力できる点が強みだ。
| 項目 | backtesting.py | backtrader | |---|---|---| | 学習コスト | 低(クラス1つで動く) | 中〜高(Cerebro・Feed設計が複雑) | | 高速処理 | 中(純Python) | 中(同等) | | マルチアセット | 非対応(単一銘柄) | 対応 | | ライブ取引連携 | 非対応 | 対応(IBKR等) | | 可視化 | Bokeh製インタラクティブHTML | matplotlib(静的) | | 最適化 | 組み込みあり | 組み込みあり |
結論として、単一通貨ペアのシステム検証・プロトタイプ作成にはbacktesting.pyが速い。複数ペアの同時管理やライブ取引への移行を見据えるならbacktraderやvectorbtが適切な選択肢になる。
1-2. インストール方法
pip install backtesting
# 依存ライブラリを含めた安定版を指定する場合
pip install backtesting==0.3.3 pandas numpy bokeh
Pythonバージョンは3.8以上を推奨する。Google Colabでも動作するため、ローカル環境を用意できない場合はColabで試すことができる。
2. FXデータの準備方法
2-1. Yahoo Finance からの取得(無料)
yfinanceライブラリを使えばOHLCV形式のデータを無料で取得できる。ただし、FX(Forex)の無料データは日足が限界であり、分足・時間足は有料データソースが必要になる。
import yfinance as yf
import pandas as pd
# USD/JPY の日足データを取得(2020-01-01 〜 2024-12-31)
ticker = yf.Ticker("USDJPY=X")
df = ticker.history(start="2020-01-01", end="2024-12-31")
# backtesting.py が要求するカラム名に変換(Open/High/Low/Close/Volume)
df = df[["Open", "High", "Low", "Close", "Volume"]]
df.index = pd.to_datetime(df.index)
df = df.dropna()
print(df.shape) # 例: (1258, 5)
print(df.tail(3))
2-2. OANDA API を使った分足データ取得
実運用に近い検証には時間足以下のデータが必要だ。OANDAのデモ口座(無料)を取得すればoandapyV20ライブラリで分足・時間足データを取得できる。
from oandapyV20 import API
from oandapyV20.endpoints.instruments import InstrumentsCandles
import pandas as pd
ACCESS_TOKEN = "your_access_token" # OANDAデモアカウントのトークン
client = API(access_token=ACCESS_TOKEN, environment="practice")
params = {
"granularity": "H1", # 時間足(M1=1分、H4=4時間、D=日足)
"count": 5000,
"price": "M" # Mid price
}
r = InstrumentsCandles(instrument="USD_JPY", params=params)
client.request(r)
candles = r.response["candles"]
records = []
for c in candles:
if c["complete"]:
records.append({
"Open": float(c["mid"]["o"]),
"High": float(c["mid"]["h"]),
"Low": float(c["mid"]["l"]),
"Close": float(c["mid"]["c"]),
"Volume": int(c["volume"])
})
df = pd.DataFrame(records)
df.index = pd.to_datetime([c["time"] for c in candles if c["complete"]])
2-3. データ品質チェックの重要性
バックテスト精度はデータ品質に直結する。以下の確認を必ず実施する。
# 欠損値・重複インデックスの確認
print("欠損:", df.isnull().sum())
print("重複:", df.index.duplicated().sum())
# High >= Close >= Low の整合性確認
assert (df["High"] >= df["Close"]).all(), "High < Close の行が存在"
assert (df["Close"] >= df["Low"]).all(), "Close < Low の行が存在"
3. Strategyクラスの基本構造
3-1. init メソッドと next メソッドの役割
backtesting.pyの中核はStrategyクラスである。必ずオーバーライドする2つのメソッドがある。
from backtesting import Strategy
class MyStrategy(Strategy):
def init(self):
"""
バックテスト開始前に一度だけ呼ばれる。
インジケーターの計算・self.I() によるラッピングをここで行う。
self.I() を使わないとベクトル化計算が適用されず速度が落ちる。
"""
pass
def next(self):
"""
各バー(ローソク足)ごとに呼ばれる。
エントリー・エグジットの判定ロジックをここに書く。
self.buy() / self.sell() でポジションを操作する。
"""
pass
3-2. self.I() によるインジケーター計算
self.I()は関数を受け取りインジケーター系列を返すラッパーである。backtesting.py内部でベクトル計算を最適化し、プロット時に自動でチャートへ描画される。
import pandas_ta as ta
import numpy as np
class RsiStrategy(Strategy):
rsi_period = 14
upper = 70
lower = 30
def init(self):
close = self.data.Close
# pandas_ta の RSI を self.I() でラップする
self.rsi = self.I(
lambda x: ta.rsi(pd.Series(x), length=self.rsi_period).values,
close
)
4. RSIを使ったシンプルな売買戦略の実装
4-1. エントリー・エグジットロジック
RSIが30を下回ったら買い(過売り)、70を超えたら売り(過買い)とするシンプルな逆張り戦略を実装する。このロジックが実際の相場で有効かどうかはデータによって大きく異なる。
from backtesting import Backtest, Strategy
import pandas as pd
import numpy as np
def rsi_calc(close, period=14):
"""
pandas_ta 非依存のRSI計算(Wilder平滑化移動平均方式)
"""
delta = pd.Series(close).diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss
return (100 - 100 / (1 + rs)).values
class RsiFxStrategy(Strategy):
rsi_period = 14 # 最適化対象パラメータ
upper = 70 # 同上
lower = 30 # 同上
sl_pips = 50 # ストップロス(pips単位の近似、USD/JPY想定)
tp_pips = 100 # テイクプロフィット
def init(self):
self.rsi = self.I(rsi_calc, self.data.Close, self.rsi_period)
def next(self):
price = self.data.Close[-1]
sl_price = price * (1 - self.sl_pips / 10000)
tp_price = price * (1 + self.tp_pips / 10000)
if not self.position:
if self.rsi[-1] < self.lower:
# 過売り → 買いエントリー
self.buy(sl=sl_price, tp=tp_price)
elif self.rsi[-1] > self.upper:
# 過買い → 売りエントリー
sl_price_s = price * (1 + self.sl_pips / 10000)
tp_price_s = price * (1 - self.tp_pips / 10000)
self.sell(sl=sl_price_s, tp=tp_price_s)
4-2. bt.run() によるバックテスト実行
bt = Backtest(
data=df, # OHLCV DataFrame
strategy=RsiFxStrategy,
cash=1_000_000, # 初期資金(円)
commission=0.0002, # スプレッド相当(0.02%)
exclusive_orders=True # 同時に1ポジションのみ
)
stats = bt.run()
print(stats)
bt.run()が返すstatsオブジェクトの主要指標:
| 指標 | 意味 | 目安 |
|---|---|---|
| Return [%] | バックテスト期間の総リターン | 正値 |
| Sharpe Ratio | リスク調整後リターン | 1.0以上が目安 |
| Max. Drawdown [%] | 最大ドローダウン | -20%以内が一つの基準 |
| Win Rate [%] | 勝率 | 単体では意味を持たない |
| # Trades | 総取引数 | 統計的有意には30件以上 |
| Profit Factor | 総利益 / 総損失 | 1.25以上が最低ライン |
4-3. bt.plot() のインタラクティブHTMLチャートの読み方
# HTMLファイルとして保存(Colab では open=False を指定)
bt.plot(filename="rsi_fx_backtest.html", open=False)
bt.plot()が出力するBokeh製のHTMLチャートは複数のパネルで構成される。ブラウザで開いた際に各パネルが何を表しているかを理解することが、戦略の問題点を発見するうえで不可欠だ。
パネル1: エクイティカーブ(上部・最も重要)
縦軸が資産残高(初期資金を1.0とした場合もある)、横軸が時間軸である。右肩上がりが理想だが、上昇の形状にも注目する必要がある。急角度で上昇した後に急落するパターンは、特定の相場環境(例:一方向のトレンド相場)にだけ適合しているサインであることが多い。曲線が滑らかに上昇するほど、戦略が多様な相場局面に適応できていると解釈できる。
また、エクイティカーブの上に三角形(▲▼)のマーカーが重なって表示される。上向き三角(▲)が買いエントリー、下向き三角(▼)が売りエントリーを示す。利益確定で閉じたポジションは色違いのマーカーで区別される実装が多い。エントリータイミングが相場の転換点付近に集中しているかを目視で確認することは、ロジックが機能しているかの直感的な検証になる。
パネル2: ドローダウンチャート(中段)
このパネルは最大ドローダウンからの回復曲線を示す。縦軸は0から負方向(-X%)で表示される。横軸方向の幅が広い(回復に時間がかかっている)箇所は、実運用時に最もメンタル負荷がかかる区間だ。深さだけでなく、持続期間を必ず確認する。たとえばMDDが-15%でも、その状態が12ヶ月間続くようであれば、実運用での継続は心理的に難しい。
パネル3: インジケーターパネル(下部)
self.I()でラップしたインジケーター(本稿ではRSI)が自動描画される。RSIが30・70のライン(閾値)を割り込んだタイミングとエクイティカーブのマーカーが一致しているかを照合することで、ロジックが意図通りに実装されているかを検証できる。ズレが生じている場合、self.I()への引数の渡し方に問題がある可能性を疑う。
ツールバー機能
チャート右上のツールバーにはパン・ズーム・リセット・ホイールズームが用意されている。特定の期間(例:COVID-19ショック前後の2020年2〜4月)に絞り込んで、その局面での取引がどのように行われたかを詳細確認できる。
5. RSI + ボリンジャーバンド複合戦略の実装
5-1. 設計思想
単一インジケーターの戦略は、そのインジケーターが苦手な相場局面(例:RSI逆張りはトレンド相場に弱い)で大きく負ける傾向がある。そこで、ボリンジャーバンドを組み合わせて「RSIが過売りシグナル かつ バンド下限に接触した場合のみエントリー」という複合条件を設ける。複合条件はトレード数を減らすが、シグナルの信頼性を高める方向に働く。
ただし、この複合条件の追加が実際にパフォーマンスを改善するかどうかは、バックテストで検証するまでわからない。以下は実装例であり、有効性を保証するものではない。
5-2. 実装コード
from backtesting import Backtest, Strategy
import pandas as pd
import numpy as np
def rsi_calc(close, period=14):
delta = pd.Series(close).diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss
return (100 - 100 / (1 + rs)).values
def bb_upper(close, period=20, std_dev=2.0):
s = pd.Series(close)
ma = s.rolling(period).mean()
sd = s.rolling(period).std()
return (ma + std_dev * sd).values
def bb_lower(close, period=20, std_dev=2.0):
s = pd.Series(close)
ma = s.rolling(period).mean()
sd = s.rolling(period).std()
return (ma - std_dev * sd).values
def bb_mid(close, period=20):
return pd.Series(close).rolling(period).mean().values
class RsiBbStrategy(Strategy):
rsi_period = 14
rsi_upper = 70
rsi_lower = 30
bb_period = 20
bb_std = 2.0
sl_pips = 60
tp_pips = 120
def init(self):
close = self.data.Close
self.rsi = self.I(rsi_calc, close, self.rsi_period)
self.bb_up = self.I(bb_upper, close, self.bb_period, self.bb_std)
self.bb_lo = self.I(bb_lower, close, self.bb_period, self.bb_std)
self.bb_mid = self.I(bb_mid, close, self.bb_period)
def next(self):
price = self.data.Close[-1]
if self.position:
return # ポジション保有中は新規エントリーしない
# 買いシグナル: RSI過売り かつ 終値がBBロウアー以下
if self.rsi[-1] < self.rsi_lower and price <= self.bb_lo[-1]:
sl = price * (1 - self.sl_pips / 10000)
tp = price * (1 + self.tp_pips / 10000)
self.buy(sl=sl, tp=tp)
# 売りシグナル: RSI過買い かつ 終値がBBアッパー以上
elif self.rsi[-1] > self.rsi_upper and price >= self.bb_up[-1]:
sl = price * (1 + self.sl_pips / 10000)
tp = price * (1 - self.tp_pips / 10000)
self.sell(sl=sl, tp=tp)
5-3. 単独戦略との比較
RSI単独とRSI+BBの組み合わせを同一データで比較した際、複合条件を追加するとトレード数は一般的に40〜60%程度減少する。その代わり、バンドタッチという「価格が統計的に外れている」という根拠が加わるため、1トレードあたりの期待値が改善するケースが多い(要検証:バックテスト上での傾向であり、フォワード期間での再現性は保証されない)。
6. sizeパラメータを使った資金管理
6-1. 固定ロット vs 固定リスク%
資金管理はバックテストの設計において非常に重要な要素であるにもかかわらず、軽視されがちだ。backtesting.pyではself.buy()・self.sell()のsize引数で1トレードのロットサイズを制御できる。
方法1:固定ロット(デフォルト動作)
# size を指定しない場合、利用可能資金の全額でポジションを取る
# FX検証では明示的に固定ロットを指定するのが安全
self.buy(size=10000, sl=sl, tp=tp) # 例: 10,000通貨固定
固定ロットは計算がシンプルで直感的だが、口座残高が変動してもリスク量が変化しない。連勝時にレバレッジが低下し、連敗時にレバレッジが増大するという特性がある。
方法2:固定リスク%(推奨)
各トレードで口座残高の一定割合をリスクにさらす方法だ。例えば口座残高の2%をリスクとし、ストップロスまでの距離からロットサイズを逆算する。
class FixedRiskStrategy(Strategy):
rsi_period = 14
rsi_lower = 30
rsi_upper = 70
sl_pips = 50
tp_pips = 100
risk_pct = 0.02 # 口座残高の2%をリスクとする
def init(self):
self.rsi = self.I(rsi_calc, self.data.Close, self.rsi_period)
def next(self):
price = self.data.Close[-1]
equity = self.equity # 現在の口座残高
sl_dist = price * self.sl_pips / 10000 # SLまでの価格差(円)
# リスク金額 / SLまでの距離 = ロットサイズ
risk_amount = equity * self.risk_pct
lot_size = int(risk_amount / sl_dist) # 整数ロットに切り捨て
if not self.position:
if self.rsi[-1] < self.rsi_lower:
sl = price - sl_dist
tp = price + price * self.tp_pips / 10000
self.buy(size=lot_size, sl=sl, tp=tp)
elif self.rsi[-1] > self.rsi_upper:
sl = price + sl_dist
tp = price - price * self.tp_pips / 10000
self.sell(size=lot_size, sl=sl, tp=tp)
6-2. 資金管理方式がパフォーマンスに与える影響
固定リスク%方式は、利益が積み重なると自動的にロットサイズが増加する(複利効果)。逆に損失が続けば自動的にロットサイズが縮小し、破産リスクを低減する。これはケリー基準の現実的な近似として機能する。
ただし、バックテストで固定リスク%を使う場合、初期資金の設定がトレード数に影響する点に注意が必要だ。lot_sizeが0になると注文が通らなくなるため、max(lot_size, 1000)等のフロアを設けておくことを筆者は推奨している。
7. backtesting.py 0.3.3 の既知の落とし穴
7-1. ルックアヘッドバイアス
最も深刻な問題がルックアヘッドバイアスだ。self.I()の外でインジケーターを計算した場合、next()内でそのバーよりも「未来の」データを誤って参照することがある。
# 危険な実装例(ルックアヘッドバイアスが発生する可能性)
class BadStrategy(Strategy):
def init(self):
# self.I() を使わず、全系列を一度に計算している
self.rsi_series = rsi_calc(self.data.Close, 14) # numpy array
def next(self):
# self.rsi_series[-1] は「現在バー時点での最新値」を返すが、
# numpy配列に対しての [-1] は実際には配列末尾(未来の値)を返す
if self.rsi_series[-1] < 30: # ← これは未来参照になっている
self.buy()
正しくはself.I()でラップし、next()内でself.rsi[-1]を参照する。self.I()でラップされた配列はバーごとにスライスされるため、「現在バーまでの値」しか見えない設計になっている。
7-2. Index Alignment の問題
pandasのDataFrameをそのままself.I()に渡すと、インデックスのアライメントがずれてNaNが発生することがある。特にpandas_ta等のライブラリが返すSeriesはIndexを保持しているため、以下のように.valuesでnumpy配列に変換してから渡すことを徹底する。
# 正しい書き方
self.rsi = self.I(
lambda x: ta.rsi(pd.Series(x), length=14).values, # .values が重要
self.data.Close
)
# 問題が起きやすい書き方(Seriesをそのまま返す)
self.rsi = self.I(
lambda x: ta.rsi(pd.Series(x), length=14), # Seriesを返すとアライメントがずれる
self.data.Close
)
7-3. exclusive_orders=True の挙動
Backtest(..., exclusive_orders=True)を指定した場合、既存ポジションのSL/TPが自動でキャンセルされたうえで新規注文が入る。next()内でif not self.position:の条件を書かなくても重複ポジションにはならないが、意図しないタイミングで前のポジションが強制クローズされる可能性がある。複数ポジションの管理が必要な戦略ではFalseに設定し、if not self.position:を明示的に書くほうが安全だ。
7-4. コミッション計算の注意点
commission=0.0002は取引額の0.02%が片道コストとして引かれる。往復では0.04%だ。USD/JPYの1ロット(100,000通貨)で価格が150円なら往復コストは約60円。これは0.04pipに相当し、実際のスプレッドよりも小さい可能性がある。スプレッドが広がる経済指標前後のコストを過小評価しないよう、コミッションを実際のスプレッドの平均値に合わせることを検討する。
8. Google Colab での動作確認手順
8-1. セルごとにコピペで動くコード
ローカル環境を構築できない場合でも、Google Colabで即座に動作確認できる。以下のセルを順番に実行する。
セル1: ライブラリのインストール
!pip install backtesting yfinance -q
セル2: データ取得と前処理
import yfinance as yf
import pandas as pd
ticker = yf.Ticker("USDJPY=X")
df = ticker.history(start="2021-01-01", end="2024-12-31")
df = df[["Open", "High", "Low", "Close", "Volume"]]
df.index = pd.to_datetime(df.index)
df = df.dropna()
print(f"取得件数: {len(df)}")
df.tail()
セル3: RSI計算関数の定義
def rsi_calc(close, period=14):
delta = pd.Series(close).diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss
return (100 - 100 / (1 + rs)).values
セル4: Strategy定義とバックテスト実行
from backtesting import Backtest, Strategy
class RsiFxStrategy(Strategy):
rsi_period = 14
upper = 70
lower = 30
sl_pips = 50
tp_pips = 100
def init(self):
self.rsi = self.I(rsi_calc, self.data.Close, self.rsi_period)
def next(self):
price = self.data.Close[-1]
sl_price = price * (1 - self.sl_pips / 10000)
tp_price = price * (1 + self.tp_pips / 10000)
if not self.position:
if self.rsi[-1] < self.lower:
self.buy(sl=sl_price, tp=tp_price)
elif self.rsi[-1] > self.upper:
self.sell(
sl=price * (1 + self.sl_pips / 10000),
tp=price * (1 - self.tp_pips / 10000)
)
bt = Backtest(df, RsiFxStrategy, cash=1_000_000, commission=0.0002, exclusive_orders=True)
stats = bt.run()
print(stats)
セル5: チャートの保存と表示
# Colab では open=False が必須(ブラウザを起動しようとするとエラーになる)
bt.plot(filename="/content/rsi_result.html", open=False)
# HTML をColabのファイルブラウザからダウンロードして確認
from google.colab import files
files.download("/content/rsi_result.html")
9. bt.run() の主要指標の目安と次のステップ
9-1. 実用的な指標の閾値
「バックテストが良い数値を出した」という感覚は人によって異なる。筆者が実装・検証してきた経験から、以下の閾値を参考値として提示する。なお、これらはひとつの評価軸であり、相場環境・検証期間・取引数によって大きく変動する点は強調しておきたい。
| 指標 | 最低ライン | 目標値 | 備考 | |---|---|---|---| | Profit Factor(PF) | 1.25 | 1.5以上 | 1.0未満は赤字確定 | | Sharpe Ratio | 0.5 | 1.0以上 | 年次換算値 | | 最大ドローダウン | -30%未満 | -15%以内 | 深いほど実運用困難 | | 勝率 | 40%以上 | 50〜60% | PFとセットで評価 | | 総取引数 | 30件 | 50件以上 | 少ないと統計的信頼性が低い |
9-2. 目標値が出なかった時の診断フロー
目標値が出ない場合、闇雲にパラメータを変えるのではなく、以下の順で原因を診断する。
ステップ1:取引数の確認
# Tradesが30件未満の場合、どんな指標も統計的に意味を持たない。戦略のシグナル条件を緩和するか、データ期間を延ばす。
ステップ2:エクイティカーブの形状確認
右肩下がり(エントリー方向の逆)の場合、売買の方向性が逆になっている可能性がある。RSI逆張りを順張りに変えてみる(RSI > 70で買い)、またはSL/TPの比率を見直す。
ステップ3:相場環境との適合性確認
bt.plot()で特定期間の取引を確認し、「どの局面では機能していてどの局面では機能していないか」を分析する。RSI逆張りはレンジ相場で機能しやすく、強トレンド相場では機能しにくい傾向がある(この傾向自体も要検証)。
ステップ4:コストの影響確認
commission=0でバックテストを再実行し、コストゼロの場合のPFを比較する。「コストゼロでPF=1.05」の場合、戦略そのものではなくコストが足を引っ張っている。スプレッドが細い通貨ペアに切り替えることも選択肢になる。
10. MT4/MT5 StrategyTester との比較・注意点
10-1. 結果が異なる主な原因
backtesting.pyとMT4/MT5のStrategyTester(ST)で同じロジックを実装しても、バックテスト結果が異なることが多い。その主な原因は以下の5点だ。
1. ティックデータの品質差
MT4/MT5のSTは「すべてのティック」モードでより細かい価格変動を使ってSL/TPを判定する。backtesting.pyはOHLC足データしか持たないため、1本のローソク足の中でSLとTPが両方到達した場合の判定が異なる。
2. スプレッドの扱い
MT4/MT5はBidとAskの両方を管理し、Askでの買い・Bidでの売りを正確にシミュレートする。backtesting.pyのMid価格ベースのシミュレーションとは異なる。commissionで近似しているが、経済指標発表時のスプレッド拡大は再現できない。
3. インジケーター計算の差異
MetaTraderのビルトインRSI(例:iRSI関数)はSmoothed Moving Average(SMMA)を使う。本稿のコードはEWM(指数加重移動平均)で実装しており、パラメータが同じでも値は微妙に異なる。
4. バーの開始価格の使用
backtesting.pyはデフォルトで「次の足のOpen価格」でエントリーが実行されるが、MT4/MT5の実装次第では現在足のClose価格でエントリーする実装になっている場合がある。これは特にスキャルピング系の戦略で大きな差を生む。
5. ストップオーダーの扱い
MT4/MT5では逆指値注文のスリッページを個別に設定できるが、backtesting.pyでは考慮されない。
10-2. 両者を使い分ける実践的アプローチ
筆者が推奨するのは「backtesting.pyでプロトタイプ検証 → MT5 STで精密検証」という二段階アプローチだ。最初からMT5のSTだけで検証しようとすると、MQL5の記述コストが高く、パラメータの探索が遅くなる。backtesting.pyでPythonの表現力を活かして高速に仮説検証を行い、有望な戦略のみMQL5に移植してMT5 STで最終確認する。この分業が実装コスト削減に効果的だ。
11. optimize() によるパラメータ最適化と過学習対策
11-1. グリッドサーチの実装
stats_opt, heatmap = bt.optimize(
rsi_period=range(10, 30, 2), # 10〜28を2刻みで探索
upper=range(65, 80, 5), # 65, 70, 75
lower=range(20, 40, 5), # 20, 25, 30, 35
maximize="Sharpe Ratio", # 最大化する指標
constraint=lambda p: p.upper > p.lower + 20, # upper > lower + 20 を強制
return_heatmap=True
)
print(stats_opt["_strategy"])
print(f"最適RSI期間: {stats_opt['_strategy'].rsi_period}")
print(f"Sharpe Ratio: {stats_opt['Sharpe Ratio']:.3f}")
11-2. 過学習(カーブフィッティング)への対処
最適化でSharpe Ratioが改善されたとしても、それが過去データへの過適合である可能性は常に存在する。以下の手法で堅牢性を検証する。
Walk-Forward Analysis の概念的実装
import warnings
warnings.filterwarnings("ignore")
# データを訓練期間(70%)と検証期間(30%)に分割
split_idx = int(len(df) * 0.7)
df_train = df.iloc[:split_idx]
df_test = df.iloc[split_idx:]
# 訓練データで最適パラメータを探索
bt_train = Backtest(df_train, RsiFxStrategy, cash=1_000_000, commission=0.0002)
stats_train, _ = bt_train.optimize(
rsi_period=range(10, 30, 2),
upper=range(65, 80, 5),
lower=range(20, 40, 5),
maximize="Sharpe Ratio",
constraint=lambda p: p.upper > p.lower + 20,
return_heatmap=True
)
best_params = {
"rsi_period": stats_train["_strategy"].rsi_period,
"upper": stats_train["_strategy"].upper,
"lower": stats_train["_strategy"].lower,
}
print("最適パラメータ:", best_params)
# 検証データで同パラメータを適用
bt_test = Backtest(df_test, RsiFxStrategy, cash=1_000_000, commission=0.0002)
stats_test = bt_test.run(**best_params)
print("\n=== 検証期間の結果 ===")
print(f"Return: {stats_test['Return [%]']:.2f}%")
print(f"Sharpe: {stats_test['Sharpe Ratio']:.3f}")
print(f"Max DD: {stats_test['Max. Drawdown [%]']:.2f}%")
訓練期間と検証期間でSharpe Ratioが大きく乖離する場合(例:訓練1.8 → 検証0.4)、過学習が強く疑われる。パラメータ空間を縮小するか、より長いデータ期間を使用して再検証する必要がある。
まとめ
backtesting.pyはFX戦略の検証ツールとして、低い学習コストと実用的な機能のバランスに優れている。本稿で示したポイントを整理する。
- インストール:
pip install backtestingのみで動作。Google Colabにも対応する - データ準備: 日足は
yfinanceで無料取得可能。分足・時間足はOANDA API等の有料ソースが現実的 - Strategyクラス:
init()でインジケーターをself.I()でラップし、next()でロジックを記述する。self.I()を省略するとルックアヘッドバイアスのリスクがある - bt.plot(): 3つのパネル(エクイティカーブ・ドローダウン・インジケーター)を精読する。エントリーマーカーとインジケーターの値を照合してロジックの動作を検証する
- 資金管理: 固定リスク%方式は
self.equityから動的にロットを計算する。固定ロットよりも長期的な破産リスクが低い - 結果解釈: PF≥1.25、Sharpe≥1.0、MDD≤-15%、取引数≥50を目安に評価する。目標値が出ない場合は取引数→方向性→相場適合性→コストの順で診断する
- 落とし穴: index alignmentとexclusive_ordersの挙動に注意し、MT4/MT5 STとの結果差異を理解したうえで使い分ける
- 最適化:
optimize()はカーブフィッティングのリスクがある。Walk-Forward Analysisで訓練/検証期間を分けて堅牢性を必ず確認する
バックテストの目的は「この戦略が過去に有効だったか」を確認することであり、将来の収益を保証するものではない。実運用への移行にはデモ口座でのフォワードテストを最低でも3ヶ月以上実施することを推奨する。
よくある質問(FAQ)
Q: backtesting.py で複数通貨ペアを同時にバックテストできますか?
A: backtesting.pyは単一銘柄のバックテストのみに対応しています。複数ペアを同時に扱いたい場合はvectorbtやbacktraderへの移行を検討してください。
Q: bt.optimize() を実行したらカーネルがクラッシュしました。
A: パラメータの組み合わせ数が多すぎる場合に発生します。rangeの刻み幅を広くするか、パラメータ数を3つ以内に絞ってから実行してください。Google Colabの場合はRAMが12GB(無料プラン)に制限されているため特に注意が必要です。
Q: RSI逆張りのバックテストが利益を出しているのに、デモ口座では損失になります。
A: スプレッドの過小評価、経済指標発表時のスリッページ、バックテスト期間の相場環境とフォワード期間の違いが主な原因として考えられます。commissionを実際の平均スプレッドの2〜3倍に設定して再検証することを推奨します。
Q: self.I() の第一引数に渡せる関数の制約はありますか?
A: 入力としてnumpy配列を受け取り、同じ長さのnumpy配列を返す関数であれば使用できます。pandasのSeriesを返す場合は.valuesで変換してください。また、追加引数はself.I(func, data, arg1, arg2, ...)のように第三引数以降に渡します。
Q: バックテスト結果のReturn [%] はスプレッドコスト控除後の数値ですか?
A: はい。Backtest()に渡したcommissionパラメータが往復のコストとして反映された後の純リターンです。ただし、スリッページは含まれないため、実際の運用成績はさらに低くなる可能性があります。
免責事項 本記事はFXバックテストの技術的手法を解説することを目的としており、特定の投資・取引を推奨するものではありません。FX取引は元本を超える損失が生じる可能性があります。バックテスト結果は過去のデータに基づくものであり、将来の利益を保証するものではありません。投資判断はご自身の責任において行い、必要に応じて金融の専門家にご相談ください。本記事の情報を利用したことによるいかなる損害についても、当メディアは責任を負いません。
