Skip to content
Go back

バックテストを並列化して7日→1日に短縮した話

Edit page

FXの自動売買システムで施策の効果を検証するたびに、9通貨ペア × 複数パラメータのグリッドサーチを回す必要がある。逐次実行では1回のグリッドサーチに7日かかっていた。

これを multiprocessing による並列処理で1日に短縮した。計画7日の工数を1日で完了し、55テスト全PASSで本番稼働している。

本記事では、Pythonのバックテストを並列化する際の設計判断、GILの回避、メモリ管理、再現性の確保について記録する。


なぜバックテストの高速化が重要か

グリッドサーチの計算量

新しいフィルターやパラメータの効果を検証するとき、以下のような組み合わせを試す。

通貨ペア: 9種類(EUR_JPY, GBP_JPY, AUD_JPY, ...)
SL倍率:  [0.5, 1.0, 1.5, 2.0]
TP倍率:  [1.0, 2.0, 3.0, 4.0, 5.0]
min_score: [1.5, 1.9, 2.3, 2.7]
期間:     2022-06-29 〜 現在(約3.5年分のH4足データ)

合計: 9 × 4 × 5 × 4 = 720パターン

1パターンあたり30秒〜1分かかるため、逐次実行では720分(12時間)。これを9通貨ペアのパラメータ組み合わせを変えながら複数回実行するため、施策検証1回に7日かかっていた。

高速化の効果


PythonのGIL問題と回避策

GIL(Global Interpreter Lock)とは

Pythonには「GIL」(Global Interpreter Lock、グローバルインタプリタロック)という仕組みがある。これはPythonのスレッドが同時に1つしかPythonコードを実行できないようにする排他ロックだ。

つまり threading モジュールでスレッドを増やしても、CPU集約的な処理(バックテストの計算)は並列に実行されない。

# これではバックテストは速くならない
import threading
threads = [threading.Thread(target=run_backtest, args=(pair,)) for pair in pairs]
for t in threads:
    t.start()  # GILにより、実質逐次実行

multiprocessingでGILを回避

multiprocessing モジュールを使うと、各プロセスが独立したPythonインタプリタとメモリ空間を持つため、GILの制約を受けない。CPUコア数分の真の並列実行が可能になる。

from multiprocessing import Pool

def run_backtest_for_params(params: dict) -> dict:
    """1パラメータセットのバックテストを実行して結果を返す"""
    pair = params["pair"]
    sl = params["sl_multiplier"]
    tp = params["tp_multiplier"]
    # ... バックテスト実行 ...
    return {"pair": pair, "sharpe": sharpe_ratio, "max_dd": max_dd}

# CPUコア数の80%を使用(システム全体を占有しない)
num_workers = max(1, int(os.cpu_count() * 0.8))

with Pool(processes=num_workers) as pool:
    results = pool.map(run_backtest_for_params, all_param_combinations)

並列化で直面した課題

課題1: メモリ消費の爆発

各プロセスが独立したメモリ空間を持つため、8プロセス並列にすると、データフレームが8倍メモリを消費する。3.5年分のH4足データ(9通貨ペア分)を全プロセスに読み込むと、メモリが枯渇した。

解決策: 各プロセスに「自分が担当する通貨ペアのデータだけ」を渡す。データの分割はメインプロセスで行い、各ワーカーには必要最小限のデータだけをシリアライズして送る。

def prepare_worker_data(pair: str) -> dict:
    """ワーカーに渡すデータを最小限に絞る"""
    df = load_pair_data(pair)  # 1ペア分のみ読み込み
    return {
        "pair": pair,
        "data": df.to_dict("records"),  # シリアライズ可能な形式に変換
    }

課題2: Parquetファイルの同時読み込み競合

複数プロセスが同じParquetファイルを同時に読み込もうとすると、ファイルロックの問題が発生する場合がある。

解決策: データの読み込みはメインプロセスで一括実行し、ワーカーにはメモリ上のデータを渡す方式に変更。

課題3: 再現性の確保

並列実行では、プロセスの実行順序が不定になる。結果の順序がバラバラになると、デバッグや過去の結果との比較が困難になる。

解決策: 結果に必ず入力パラメータのIDを含め、全結果収集後にソートする。乱数を使う場合はシード値を入力パラメータに含めて、各ワーカーで決定的に設定する。

def run_backtest_for_params(params: dict) -> dict:
    # 再現性のためにシード値を固定
    random.seed(params.get("seed", 42))
    np.random.seed(params.get("seed", 42))
    # ...

課題4: エラーハンドリング

1つのワーカーが例外で落ちると、Pool全体が影響を受ける。

解決策: 各ワーカー内で例外をキャッチし、エラー情報を結果として返す。メインプロセスでは「成功した結果」だけを集計し、「失敗した結果」はログに記録する。

def run_backtest_for_params(params: dict) -> dict:
    try:
        # バックテスト実行
        return {"status": "success", "params": params, "result": result}
    except Exception as e:
        return {"status": "error", "params": params, "error": str(e)}

実行スクリプトの設計

# 基本的な使い方
python scripts/run_fast_grid_search.py \
  --pairs EUR_JPY GBP_JPY AUD_JPY \
  --workers 4 \
  --output results/grid_search_2026_03.csv

# 全通貨ペア・全パラメータ
python scripts/run_fast_grid_search.py \
  --pairs ALL \
  --workers 8 \
  --sl-range 0.5 2.0 0.5 \
  --tp-range 1.0 5.0 1.0 \
  --score-range 1.5 2.7 0.4

結果はCSVに出力し、以下のカラムを含む。

pair, sl_multiplier, tp_multiplier, min_score,
sharpe_ratio, max_drawdown, profit_factor,
total_trades, win_rate, avg_pnl

速度改善の結果

実行方式720パターンの実行時間CPUコア使用率
逐次実行約12時間12%(1コア)
Pool(4workers)約3.5時間48%
Pool(8workers)約1.8時間80%

8ワーカーで約6.7倍の高速化を達成。理論上の8倍には達しないのは、データの分配・結果の集約・メモリバスの帯域幅がボトルネックになるためだ(アムダールの法則)。


学んだこと

1. GILを理解していないと「並列化したのに速くならない」罠にハマる

Pythonの threading はI/O待ち(ネットワーク通信、ファイル読み書き)には有効だが、CPU集約的な処理(バックテスト計算)には効果がない。multiprocessing で別プロセスにすることでGILを回避する必要がある。

2. メモリは「コア数倍」を覚悟する

multiprocessing は各プロセスがメモリを独立に持つ。8並列なら8倍のメモリが必要になりうる。データの分割・必要最小限の転送が実装の要だ。

3. 再現性は並列化の最大の敵

実行順序が不定になるため、結果の順序保証・乱数シードの管理を明示的に行う必要がある。これを怠ると「昨日と違う結果が出たが、パラメータの問題か実行順序の問題か分からない」という悪夢に陥る。


まとめ

バックテスト並列化の設計で重要なのは以下の3点だ。

  1. multiprocessingでGIL回避: threading ではCPU集約処理は速くならない。multiprocessing.Pool で真の並列実行
  2. メモリ管理: 各ワーカーに必要最小限のデータだけを渡す。全データの丸コピーはメモリ枯渇の原因
  3. 再現性の確保: 乱数シード固定・結果のIDベースソートで、並列でも決定的な結果を保証

計画7日を1日に短縮したことで、「もう1パターン試してみよう」が気軽にできるようになった。これがバックテスト高速化の最大の価値だ。


Edit page
Share this post on:

Previous Post
デイトレードのシグナル改善:セッション限定フィルターで勝率30%→37%に引き上げた
Next Post
経済指標フィルターを自動売買に組み込んだ設計と実装:3層フォールバックで止まらないシステムを作る