PineScriptバックテストをCIへ組み込む(GitHub Actions / GitLab / Docker対応ランナー)
戦略リポジトリのコミットごとにバックテストを走らせ、パリティをゲートします。リグレッションはビルドを落とし、ローカルと同一のDockerイメージをランナーで実行します。
なぜCIでパリティを担保するか
戦略はどんなコードとも同じようにドリフトする。パラメータのリネーム、ヘルパー関数のリファクタリング、見落としたPine v6のAPI変更 — そのどれもが、既存のテストを壊さずにシグナルをサイレントにずらす可能性がある。CIにパリティゲートがなければ、2ヶ月前のコミットが先週のトレードリストを変えてしまい、ライブのPnLがおかしくなって初めて気づくことになる。
リファレンストレードリストを戦略ソースと並べてコミットするという規律は、ソフトウェアを信頼性あるものにするのと同じ規律だ:期待する出力を記述し、実際の出力が乖離したらビルドを失敗させる。バックテストにおける期待出力はトレードリスト — ピン留めされた過去データに対するエントリーバー、エグジットバー、方向、サイズ、約定価格 — だ。
その比較がコミットのたびに実行されると、ラチェットが得られる:戦略の過去の動作は改善されることはあっても、不意にリグレッションすることはない。出力を変えたのがどのコミットかは exactly わかる。なぜならそのコミットでビルドが失敗したからだ。エクイティカーブに対してgit blameができる。
これはほとんどのクオンツが最初に思うよりずっと重要だ。「この戦略をテストした」と「この戦略がこれまでに生成したすべての過去シグナルの再現可能・バージョン管理されたレコードがある」の間にある差は巨大だ。前者はスクリーンショット、後者は監査証跡だ。CIは、毎回のプッシュで手動の手間をかけずに後者を維持する手段だ。
PineForgeがPine戦略でこれを可能にするのは、ランタイムがDockerイメージだからだ:Dockerが動くどこでも動き、同じ入力に対して決定論的な出力を生成し、シェルスクリプトからパースするのに安全な安定したJSONスキーマを返す。ブラウザなし、認証フローなし、バックテスト実行自体のレート制限なし。
GitHub Actionsの例
全体のワークフロー:戦略をチェックアウトし、ランタイムコンテナでC++へトランスパイルし、ピン留めしたOHLCV CSVに対してバックテストを実行し、JSONレポートを次でパースします: jqでJSONレポートをパースし、コミット済みベースラインと比較し、純損益の差分が閾値を超えたらビルドを失敗させる。
ランタイムはコンテナイメージなので、管理すべきAPIキーはありません — ジョブ内でdocker runするだけです。次を作成します: .github/workflows/backtest.ymlを作成する:
name: Backtest parity
on:
push:
branches: [main, "feat/**"]
pull_request:
jobs:
backtest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Transpile strategy to C++
run: |
docker run --rm --network=none \
-e PINEFORGE_TRANSPILE_ONLY=1 \
-v "$PWD/strategy.pine":/in/strategy.pine:ro \
ghcr.io/pineforge-4pass/pineforge-engine:latest > strategy.cpp
- name: Run backtest
run: |
docker run --rm --network=none \
-v "$PWD/strategy.cpp":/in/strategy.cpp:ro \
-v "$PWD/data/ohlcv.csv":/in/ohlcv.csv:ro \
ghcr.io/pineforge-4pass/pineforge-engine:latest \
> report.json
- name: Assert parity
run: |
ACTUAL=$(jq '.summary.net_pnl' report.json)
BASELINE=$(jq '.summary.net_pnl' baseline/report.json)
DELTA=$(echo "$ACTUAL $BASELINE" | awk '{d=$1-$2; print (d<0)?-d:d}')
THRESHOLD="0.01"
if awk "BEGIN{exit !($DELTA > $THRESHOLD)}"; then
echo "Parity drift: net_pnl delta $DELTA exceeds threshold $THRESHOLD"
diff <(jq '.trades' baseline/report.json) \
<(jq '.trades' report.json)
exit 1
fi
echo "Parity OK — delta $DELTA (threshold $THRESHOLD)"ベースラインレポートを baseline/report.jsonとしてリポジトリに保存し、戦略ソースと並べてコミットする。PRが意図的に戦略の動作を変更する場合、作成者はPRの一部としてベースラインを更新する。その差分が、何がなぜ変わったかの永続的なレコードになる。
同じワークフローはGitLab CI、Bitbucket Pipelines、あるいはDockerをサポートする任意のランナーで同一に動作する — YAMLの構文を差し替えて、シェルコマンドはそのままでよい。
終了コードとパリティの検証
PineForgeランタイムは正常実行で終了コード 0を返し、エンジンレベルのエラー(不正なPine、未サポートの組み込み関数、データファイルのパース失敗)では非ゼロを返す。CIシステムはこれらの終了コードを自動的に拾う — エンジンレベルの失敗に対して特別なハンドリングは不要だ。
パリティの検証は別の懸念事項であり、自分のワークフロー内で実装する。JSONレポートスキーマはパッチバージョンをまたいで安定している:
{
"summary": {
"total_trades": 47,
"net_pnl": 12483.50,
"max_drawdown": 3201.00,
"profit_factor": 1.82,
"sharpe_ratio": 1.34,
"win_rate": 0.617
},
"trades": [
{
"entry_bar": 142,
"exit_bar": 156,
"direction": "long",
"size": 1.0,
"entry_price": 42310.5,
"exit_price": 44820.0,
"pnl": 2509.50,
"exit_reason": "strategy.close"
}
]
}簡単な数値パリティチェックには summary.net_pnl と summary.total_trades をベースラインと比較する。厳密な取引ごとのパリティには、完全な trades配列を差分比較する — エントリー・エグジットのバーインデックス、方向、約定価格がすべて一致するはずだ。一致しない場合、差分出力がどの取引が乖離し、どのバーで、約定価格の差がどれだけだったかを正確に指摘する。
数値フィールドの実用的な閾値: 0.01(1セント、または正規化されたシリーズで1ベーシスポイント)までの浮動小数点差分を許容し、それより大きいものはすべて失敗とする。取引ごとの差分では、 entry_bar や exit_barの不一致はPnLの近さに関わらず常にハードフェイルとする — 別のバーに約定が落ちているなら、シグナルロジックが変わったことを意味する。
パリティCIを採用したチームは通常、ベースライン更新をPRラベルでゲートした別のワークフローステップとして実行する。そうすることで、意図的な戦略の改善がPRのタイムライン上に記録される — ベースラインの差分が、それを引き起こしたPineの変更と並んでコードレビューに現れる。