Pythonでバックテストする方法:戦略を書いて、結果を読むまで

Pythonでのバックテストは、価格データを読み込み、売買ルールを1本ずつの足に当てはめ、損益を積み上げる処理です。pandasだけでも数十行で書けます。ただし、約定の前提を間違えると成績が実際より良く見えるので、書き方と結果の読み方の両方を押さえておく必要があります。

バックテストの基本の流れ

どのライブラリを使っても、バックテストは次の4段階でできています。

  1. 価格データ(OHLC:始値・高値・安値・終値)を用意する
  2. 各足の時点で見えている情報だけから、売買シグナルを出す
  3. シグナルを、どの価格で約定させるかを決める
  4. 約定した取引の損益とコストを積み上げ、成績を集計する

難しいのは2と3です。ここで未来の情報を使ったり、ありえない価格で約定させたりすると、成績が実際より良く見えます。

pandasだけで書く最小のバックテスト

まず、ライブラリを使わずに移動平均クロスを書いてみます。短期の移動平均が長期を上回っている間だけ買いで持つルールです。

import pandas as pd

df = pd.read_csv("usdjpy_1h.csv", parse_dates=["time"], index_col="time")

fast = df["close"].rolling(10).mean()
slow = df["close"].rolling(50).mean()
signal = (fast > slow).astype(int)      # 1 = 買いで持つ, 0 = 持たない

# シグナルが出た足の終値で入り、次の足の値動きから損益を受け取る
position = signal.shift(1).fillna(0)
ret = df["close"].pct_change().fillna(0)

# 持ち高が変わるたびにコスト(スプレッド・手数料)を差し引く
cost = position.diff().abs().fillna(0) * 0.0002
equity = (1 + position * ret - cost).cumprod()

print(f"total return: {equity.iloc[-1] - 1:.2%}")

大事なのは shift(1) です。これを忘れると、ある足の終値を見て出したシグナルで、同じ足の値動きを取ったことになります。実際には不可能な取引なので、成績が大きく良く見えます。

自作で気をつけたい3つの落とし穴

未来の情報を使ってしまう(ルックアヘッドバイアス)

シグナルの計算に、その時点ではまだ確定していない値が混ざる誤りです。shift の付け忘れのほか、データ全体で計算した平均や標準偏差で正規化する、当日の高値・安値をエントリーの判断に使う、といった形でも起きます。

約定価格の前提が甘い

シグナルが出た足の終値で入るのか、次の足の始値で入るのかで、結果はかなり変わります。利確(TP)や損切り(SL)も同じです。窓開けで損切りラインを飛び越えたのに、ラインちょうどで約定させると、損を小さく見積もります。詳しくは約定の前提の解説にまとめています。

コストを入れていない

スプレッドと手数料を入れないと、取引回数の多い戦略ほど有利に見えます。短い時間足の戦略では、コストを入れただけで成績の符号が変わることも珍しくありません。

自分のエンジンにも、この種の誤りが4つありました。TP/SLを足の終値で約定させていた、スプレッドの符号が逆だった、などです。直したら同じ戦略の成績が最大で4%ほど動き、戦略同士の順位も入れ替わりました。

損益の合計だけで判断しない

バックテストが動いたら、次は結果の読み方です。損益の合計と勝率だけでは、次に何を直せばよいか分かりません。少なくとも次の4つを見ると、戦略の性格がはっきりします。

  • 相場環境ごとの成績:トレンドとレンジで成績が逆になる戦略はよくあります。相場環境別の分析を参照してください。
  • 取引ごとの最大含み益と最大含み損(MFE・MAE):負けトレードが「一度は利益が出ていた負け」なのか「入ってすぐ逆行した負け」なのかで、直すべき場所が出口か入口かに分かれます。MFE・MAEの解説で詳しく説明しています。
  • 決済の理由:利確・損切り・反対シグナルのどれで閉じたかの内訳です。
  • パラメータを少しずらしたときの変化:一番良い値だけが突出している場合は、過剰最適化を疑います。過剰最適化の解説を参照してください。

pandasで書いた場合、これらの集計とグラフは自分で書き足すことになります。取引の一覧(エントリー時刻・決済時刻・価格・理由)を持つ形にしておくと、後から分析しやすくなります。

Hawk Backtesterのデモ画面。相場ごとの成績、損益曲線、取引一覧が並んでいる
相場ごとの成績・損益曲線・取引ごとの分析を1画面に並べた例(Hawk Backtesterのデモ、合成データ)

hawk-btで書く場合

Hawk Backtesterでは、戦略をPythonのクラスとして書きます。戦略のコードは手元のPCで動き、ブラウザ内のエンジンとlocalhostのWebSocketでつながります。コードがサーバーに送られることはありません。

pip install -U hawk-bt
hawk-bt run my_strategy.py
from hawk_bt import Strategy, Context, Param

class MaCross(Strategy):
    fast = Param(10, min=3, max=50, step=1)    # アプリにスライダーとして出る
    slow = Param(50, min=10, max=200, step=5)

    async def step(self, ctx: Context) -> None:
        i = int(ctx.state.snapshot.step)
        fast, slow = self.fast, self.slow  # Param は int として読める
        if i < slow + 1:
            return
        close = ctx.state.candles.close[:i]     # 確定した直前の足までの終値
        f_now, s_now = close[-fast:].mean(), close[-slow:].mean()
        f_prev, s_prev = close[-fast - 1:-1].mean(), close[-slow - 1:-1].mean()

        if f_prev <= s_prev and f_now > s_now:  # ゴールデンクロス
            price = ctx.state.snapshot.price
            await ctx.engine.place_ticket(
                side="buy", units=1,
                take_profit=price * 0.01,          # TP/SLは価格差で指定する
                stop_loss=price * 0.005,
            )

この書き方には、自作の場合と比べて次の違いがあります。

  • 未来の足が見えない:step の時点で戦略が見られるのは現在の足までです。
  • 約定の前提が決まっている:TP/SLはその価格で約定し、窓開けで飛び越えた場合は始値で約定します。同じ足でTPとSLの両方に届いた場合は、不利なSLを先とみなします。スプレッド・手数料・スリッページも入ります。仕様はシミュレーションモデルで公開しています。
  • パラメータがスライダーになる:Param で宣言した値は、アプリの結果画面にスライダーとして出ます。動かすと、手元のコードがその値で再実行されます。
  • 保存するたびに再実行される:ファイルを保存すると、バックテストが自動でやり直されます。前回の実行と比べて、増えた取引・消えた取引・結果が変わった取引が表示されます。
前回の実行との差分。結果が変わった取引が一覧で表示されている
戦略を保存したときに出る、前回の実行との差分

差分が出ると、損益の合計が少し良くなっただけの変更でも、中身を確かめられます。たとえば利確幅を1%から2%に広げたとき、取引数は60件のままでも、20件の結果が変わり、その約半分は悪化していました。合計だけ見ていたら「少し良くなった」で終わっていた変更です。

データはどう用意するか

バックテストには価格データが必要です。Hawk Backtesterには市場データを同梱していません。次のどれかで用意します。

  • CSVをアップロードする
  • トレンド・レンジ・急変動を切り替えた合成データを生成する
  • 自分のデータ提供元のAPIキー(Twelve Dataなど)で取り込む。キーは暗号化して保存し、取り込みにだけ使います

自作の場合も、データの時刻の扱い(タイムゾーン、足の始まりと終わりのどちらで記録しているか)は最初に確認してください。ここがずれると、ルックアヘッドバイアスの原因になります。

できないこと

Hawk Backtesterは現在、1回のバックテストで1銘柄だけを扱います。ポートフォリオの検証や、ティックデータでの検証にはまだ対応していません。アプリはPC向けで、スマートフォンでは登録不要のデモだけが快適に動きます。料金はプランのページに載せています。無料プランでもバックテストの回数に上限はありません。

よくある質問

Pythonのバックテストライブラリは何を使えばよいですか?

目的によります。ノートブックで大量のパラメータを高速に回したいならvectorbt、1本の戦略を素直に書きたいならbacktesting.pyやBacktraderが候補です。比較はバックテストツールの選び方にまとめています。

バックテストの結果が良ければ、実際の取引でも勝てますか?

保証はありません。バックテストは過去のデータに対する検証で、約定やコストの前提にも左右されます。結果は、戦略の性格を知る材料として使ってください。

hawk-btでは戦略のコードがアップロードされますか?

されません。コードは手元のPCで動き、ブラウザ内のエンジンには売買の指示だけが届きます。