Python backtrader FX バックテスト入門:移動平均クロス戦略を完全実装
FX取引においてバックテストは戦略の有効性を定量的に検証する唯一の手段だ。感覚や経験則に頼るのではなく、過去の価格データに対して戦略を適用し、シャープレシオ・最大ドローダウン・勝率といった統計指標で評価する。本稿ではPythonのバックテストフレームワーク backtrader を使い、FX通貨ペアへの移動平均クロス戦略を一から実装する手順を解説する。バックテストはあくまで過去データへの検証であり、将来の利益を保証するものではないことを前提として読み進めてほしい。
1. backtraderとは何か:他ライブラリとの比較
1-1. backtraderの設計思想
backtraderは2015年にオープンソース化されたPythonのイベント駆動型バックテストフレームワークだ。「Cerebro(脳)」と呼ばれるエンジンがデータフィード・ストラテジー・ブローカー・アナライザーを統合管理し、ユーザーはストラテジークラスの next() メソッドにロジックを記述するだけでバックテストを実行できる。
イベント駆動方式のため、ベクトル化計算を前提とするライブラリと比べてルックアヘッド・バイアス(未来データの参照)を構造的に排除しやすい。これはFX戦略の現実的な再現性を高める上で重要な特性だ。
1-2. 主要ライブラリ比較
| ライブラリ | 方式 | 速度 | FXデータ対応 | 学習コスト | |---|---|---|---|---| | backtrader | イベント駆動 | 中 | CSVで可能 | 中 | | vectorbt | ベクトル化 | 高速 | 高 | 低〜中 | | zipline | イベント駆動 | 中 | 株式中心 | 高 | | pandas | 手動実装 | 高速 | 自由 | 高 |
vectorbtは数百万バーの高速検証に向くが、複雑なポジション管理や注文タイプの再現はbacktraderの方が容易だ。ziplineはQuantopianの流れを汲むが現在はメンテナンスが低調で、FXデータの整備コストが高い。backtraderは中規模のFXバックテストにおいてバランスが取れた選択肢と言える。
2. インストールと基本セットアップ
2-1. インストール手順
Python 3.8以上の環境を前提とする。仮想環境の使用を強く推奨する。
# 仮想環境の作成と有効化
python -m venv bt_env
source bt_env/bin/activate # Windows: bt_env\Scripts\activate
# backtraderのインストール
pip install backtrader
# データ処理・可視化用の依存ライブラリ
pip install pandas matplotlib
インストール後、動作確認として以下を実行する。
import backtrader as bt
print(bt.__version__)
# 例: 1.9.78.123
2-2. 環境確認と注意点
backtraderはmatplotlibを用いて結果をプロットする。ヘッドレス環境(サーバー上)で実行する場合は matplotlib.use('Agg') でバックエンドを切り替えるか、cerebro.run() 後の cerebro.plot() 呼び出しを省略する必要がある。
また、backtraderはPython 3.xで動作するが、公式のメンテナンスが2022年以降停滞しているため、Python 3.11以降では一部の依存関係で警告が出ることがある。実用上の支障はないが、本番環境への適用前に動作検証を行うこと。
3. FXデータの読み込み方法
3-1. CSVデータの準備
FXの価格データはメタトレーダー4/5やDukascopyなどから無料でCSV形式を取得できる。backtraderが標準で受け付けるCSV形式は以下のカラム構成だ。
datetime,open,high,low,close,volume
2024-01-02 00:00:00,144.350,144.520,144.300,144.480,12500
2024-01-02 01:00:00,144.480,144.610,144.420,144.550,9800
...
volume はFX現物では取引量が非公開のため、0や1を入れてプレースホルダーとして扱うことが多い。
3-2. GenericCSVDataを使ったデータフィード
import backtrader as bt
import datetime
# GenericCSVDataでFXのCSVを読み込む
data = bt.feeds.GenericCSVData(
dataname='USDJPY_H1_2024.csv',
dtformat='%Y-%m-%d %H:%M:%S',
datetime=0,
open=1,
high=2,
low=3,
close=4,
volume=5,
openinterest=-1, # 未使用カラムは -1 で無効化
fromdate=datetime.datetime(2024, 1, 1),
todate=datetime.datetime(2024, 12, 31),
timeframe=bt.TimeFrame.Minutes,
compression=60 # 60分足
)
fromdate / todate でバックテスト期間を明示的に指定することで、データの範囲外参照を防ぐ。
4. ストラテジークラスの実装
4-1. 移動平均クロス戦略の設計
移動平均クロス(Golden Cross / Dead Cross)は検証コストが低く、パラメータ過剰フィッティングのリスクを評価しやすい入門向け戦略だ。ここでは短期EMA(指数移動平均)と長期EMAのクロスでエントリー・エグジットを行う実装を示す。
- エントリー条件(買い): 短期EMA > 長期EMA(ゴールデンクロス)
- エントリー条件(売り): 短期EMA < 長期EMA(デッドクロス)
- エグジット: 反対シグナル発生時
4-2. ストラテジークラスの実装コード
import backtrader as bt
class EMACrossStrategy(bt.Strategy):
"""
EMAクロス戦略
params:
fast : 短期EMAの期間(デフォルト: 12)
slow : 長期EMAの期間(デフォルト: 26)
"""
params = (
('fast', 12),
('slow', 26),
)
def __init__(self):
# EMAインジケーターの定義
self.ema_fast = bt.indicators.EMA(
self.data.close, period=self.params.fast
)
self.ema_slow = bt.indicators.EMA(
self.data.close, period=self.params.slow
)
# クロスオーバーシグナル(+1: ゴールデンクロス, -1: デッドクロス)
self.crossover = bt.indicators.CrossOver(
self.ema_fast, self.ema_slow
)
def next(self):
# ポジション保有中はエントリーしない
if self.position:
if self.crossover < 0:
self.close() # 買いポジションのクローズ
else:
if self.crossover > 0:
self.buy() # 買いエントリー
params をクラス変数として定義することで、後述のパラメータ最適化(optstrategy)と組み合わせることができる。
4-3. ブローカー設定とスプレッドの考慮
FXバックテストではスプレッドのコスト計算が重要だ。コストを無視したバックテスト結果は実運用と大きく乖離する。
cerebro = bt.Cerebro()
# ブローカー設定
cerebro.broker.setcash(1_000_000) # 初期資金: 100万円
cerebro.broker.setcommission(
commission=0.00003, # スプレッド3pips相当(ドル円)
commtype=bt.CommInfoBase.COMM_PERC, # 比率指定
stocklike=False # FXモード
)
# ポジションサイジング: 固定ロット
cerebro.addsizer(bt.sizers.FixedSize, stake=10000) # 1万通貨
cerebro.adddata(data)
cerebro.addstrategy(EMACrossStrategy, fast=12, slow=26)
5. バックテスト結果の評価
5-1. 評価指標の追加
backtraderはアナライザー(Analyzer)機能でシャープレシオ・最大ドローダウン・トレード統計を自動計算できる。
# アナライザーの登録
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe',
riskfreerate=0.001) # 無リスク金利: 0.1%/年
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
# バックテスト実行
results = cerebro.run()
strat = results[0]
# 結果の取得と表示
sharpe = strat.analyzers.sharpe.get_analysis()
dd = strat.analyzers.drawdown.get_analysis()
trades = strat.analyzers.trades.get_analysis()
print(f"シャープレシオ: {sharpe['sharperatio']:.3f}")
print(f"最大ドローダウン: {dd.max.drawdown:.2f}%")
print(f"総トレード数: {trades.total.total}")
print(f"勝率: {trades.won.total / trades.total.total * 100:.1f}%")
print(f"最終資産: {cerebro.broker.getvalue():,.0f}円")
5-2. 評価指標の解釈
| 指標 | 意味 | 目安 | |---|---|---| | シャープレシオ | リスク1単位あたりのリターン | 1.0以上が実用水準 | | 最大ドローダウン | 資産曲線のピークから谷への最大下落率 | 20%以内が許容目安 | | 勝率 | 勝ちトレード数 ÷ 総トレード数 | 単独では評価不能 | | プロフィットファクター | 総利益 ÷ 総損失 | 1.3以上が目安 |
勝率は単独では意味をなさない。勝率30%でも1勝の利益が1敗の損失の3倍超であればプロフィットファクターは1.0を超える。シャープレシオと最大ドローダウンの組み合わせで総合評価することが重要だ。
5-3. 完全なバックテストスクリプト
以下に一連の処理をまとめたスクリプトを示す。
import backtrader as bt
import datetime
class EMACrossStrategy(bt.Strategy):
params = (('fast', 12), ('slow', 26),)
def __init__(self):
self.ema_fast = bt.indicators.EMA(self.data.close, period=self.params.fast)
self.ema_slow = bt.indicators.EMA(self.data.close, period=self.params.slow)
self.crossover = bt.indicators.CrossOver(self.ema_fast, self.ema_slow)
def next(self):
if self.position:
if self.crossover < 0:
self.close()
else:
if self.crossover > 0:
self.buy()
def run_backtest(csv_path: str, fast: int = 12, slow: int = 26):
cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(
dataname=csv_path,
dtformat='%Y-%m-%d %H:%M:%S',
datetime=0, open=1, high=2, low=3, close=4, volume=5,
openinterest=-1,
fromdate=datetime.datetime(2024, 1, 1),
todate=datetime.datetime(2024, 12, 31),
timeframe=bt.TimeFrame.Minutes,
compression=60
)
cerebro.adddata(data)
cerebro.addstrategy(EMACrossStrategy, fast=fast, slow=slow)
cerebro.broker.setcash(1_000_000)
cerebro.broker.setcommission(commission=0.00003, commtype=bt.CommInfoBase.COMM_PERC)
cerebro.addsizer(bt.sizers.FixedSize, stake=10000)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.001)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
results = cerebro.run()
strat = results[0]
sharpe = strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')
dd_max = strat.analyzers.drawdown.get_analysis().max.drawdown
trade_info = strat.analyzers.trades.get_analysis()
total = trade_info.total.total
won = trade_info.won.total if hasattr(trade_info.won, 'total') else 0
winrate = won / total * 100 if total > 0 else 0
print(f"--- バックテスト結果 (EMA {fast}/{slow}) ---")
print(f"シャープレシオ : {sharpe:.3f}" if isinstance(sharpe, float) else f"シャープレシオ : {sharpe}")
print(f"最大ドローダウン : {dd_max:.2f}%")
print(f"総トレード数 : {total}")
print(f"勝率 : {winrate:.1f}%")
print(f"最終資産 : {cerebro.broker.getvalue():,.0f}円")
cerebro.plot(style='candlestick')
if __name__ == '__main__':
run_backtest('USDJPY_H1_2024.csv', fast=12, slow=26)
6. バックテストの限界と次のステップ
6-1. オーバーフィッティングのリスク
バックテストで良好な結果が出ても、それは過去データへの適合に過ぎない。パラメータを後付けで最適化した戦略がフォワードテスト(実際の運用期間での検証)で崩壊するケースは多い。EMAの期間を1から200まで総当たり最適化すれば、過去データにはほぼ必ず高シャープレシオの組み合わせが見つかる。これはデータマイニング・バイアスと呼ばれる。
対策として、バックテスト期間(in-sample)と検証期間(out-of-sample)を分離するウォークフォワード分析の実施を推奨する。backtraderにはこの機能が標準搭載されていないため、手動でデータ期間を分割して複数回実行するか、専用ライブラリ(vectorbt等)との組み合わせを検討する。
6-2. 実装コストの現実
バックテストはバックテスト。実運用への移行には以下のギャップが存在する。
- スリッページ: 指値注文がCSVの始値通りに約定するとは限らない
- 流動性: ニュース前後のスプレッド拡大・約定拒否
- システムリスク: サーバーダウン・ネットワーク遅延
バックテストの検証を終えた後は、デモ口座でのフォワードテストを最低3ヶ月実施してから本番移行を検討すること。
まとめ
本稿ではbacktraderを用いたFXバックテストの基本的な実装を解説した。要点を整理する。
- backtraderはイベント駆動型でルックアヘッド・バイアスを構造的に回避できる
GenericCSVDataでFXのCSV価格データを柔軟に読み込めるStrategyクラスのnext()にロジックを記述し、Analyzerで統計指標を取得する- 評価はシャープレシオ・最大ドローダウン・プロフィットファクターの複合評価が必須
- バックテスト結果は過去データへの適合であり、将来の収益を保証しない
バックテストは戦略を「棄却するためのプロセス」として使うのが合理的だ。統計的に明らかに機能しない戦略を除外し、生き残った戦略をフォワードテストで厳密に検証する。この二段階の検証を経て初めて、実運用への移行を判断できる。
免責事項: 本記事はFXバックテストの技術的な解説を目的としており、特定の投資戦略の有効性を保証するものではありません。FX取引はレバレッジを用いるため、投資元本を超える損失が発生する可能性があります。実際の取引にあたっては、ご自身のリスク許容度を十分に検討し、金融商品取引業者の提供する説明資料を確認の上、自己責任で判断してください。過去のバックテスト結果は将来の運用成果を約束するものではありません。
