本文へスキップ

エンジンCI:Pine戦略311/312が「優秀」

残り1本は登録済みの既知の差異(琥珀色)。

複合戦略インジケーター注文とリスク時間足とセッションバー内約定Pine言語アナライザーのチェック
複合戦略インジケーター注文とリスク時間足とセッションバー内約定Pine言語アナライザーのチェック

タップやドラッグで各点の値を確認できます。

出典:エンジンのmainブランチでの公開コーパスのパリティチェック(2026-09-29)。

グラフのデータを表で見る

mainブランチのエンジンによる公開コーパスのパリティチェック(2026-09-29):312本中311本が「優秀」です。残る1本は、異常として宣言されています。

グループ戦略優秀異常として宣言済み
複合戦略53530
インジケーター79790
注文とリスク77770
時間足とセッション28280
バー内約定880
Pine言語56560
アナライザーのチェック11101

PineForgeがCIでPine Scriptをバックテスト。取引がずれたらCI失敗。

なぜCIでパリティを担保するか

戦略も、ほかのコードと同じようにドリフトします。パラメータのリネーム、ヘルパー関数のリファクタリング、見落としていたPine v6のAPI変更。どれも既存のテストを壊すことなく、シグナルを知らないうちにずらす可能性があります。CIにパリティチェックがなければ、2か月前のコミットが先週の取引一覧を変えてしまっても、ライブ運用の損益がおかしくなるまで気づけません。

リファレンスとなる取引一覧を戦略のソースと一緒にコミットするのは、ソフトウェアの信頼性を支えているのと同じ規律です。期待する出力を記述しておき、実際の出力がそれと食い違えばビルドを失敗させます。バックテストの場合、期待する出力とは、固定した過去データセットに対する取引一覧(エントリーバー、決済バー、売買方向、サイズ、約定価格)です。

その比較がコミットのたびに実行されると、ラチェットが得られる:戦略の過去の動作は改善されることはあっても、不意にリグレッションすることはない。出力を変えたのがどのコミットかは 正確に わかる。なぜならそのコミットでビルドが失敗したからだ。エクイティカーブに対してgit blameができる。

これはほとんどのクオンツが最初に思うよりずっと重要だ。「この戦略をテストした」と「この戦略がこれまでに生成したすべての過去シグナルの再現可能・バージョン管理されたレコードがある」の間にある差は巨大だ。前者はスクリーンショット、後者は監査証跡だ。CIは、毎回のプッシュで手動の手間をかけずに後者を維持する手段だ。

PineForgeがここに適しているのは、ランタイムがDockerイメージだからです。Dockerが動く場所ならどこでも動き、同じ入力には同じ取引を返し、jqでパースできるJSONレポートを出力します。ブラウザもログインも不要で、バックテスト自体にレート制限もありません。

GitHub Actionsの例

ワークフロー全体では、戦略をチェックアウトし、pineforge-releaseイメージでC++にトランスパイルし、固定したOHLCV CSVでバックテストを実行して、JSONレポートを次のツールでパースします:jq。続いてレポートをコミット済みのベースラインと比較し、いずれかの取引の売買方向またはバーが変わった場合や、純損益の差分が閾値を超えた場合に、ビルドを失敗させます。

ランタイムはコンテナイメージなので、管理が必要な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@v7

      # Pin the image version so results only change when you change it.
      - 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-release:1.10.0 > 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-release:1.10.0 \
            > report.json

      - name: Assert parity
        run: |
          # Every trade keeps its side and its entry and exit bars,
          # and net P&L moves by at most THRESHOLD.
          BARS='[.trades[] | [.side, .entry_bar_index, .exit_bar_index]]'
          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 [ "$(jq -c "$BARS" report.json)" != "$(jq -c "$BARS" baseline/report.json)" ]; then
            echo "Parity drift: a trade changed side, entry bar or exit bar"
          elif awk "BEGIN{exit !($DELTA > $THRESHOLD)}"; then
            echo "Parity drift: net_pnl delta $DELTA exceeds threshold $THRESHOLD"
          else
            echo "Parity OK - same sides and bars, net_pnl delta $DELTA (threshold $THRESHOLD)"
            exit 0
          fi
          # One trade per line, so diff's line numbers are trade numbers.
          TRADE='.trades[] | {side, entry_bar_index, exit_bar_index, entry_price, exit_price, qty, pnl}'
          diff <(jq -c "$TRADE" baseline/report.json) <(jq -c "$TRADE" report.json)
          exit 1

ベースラインレポートをbaseline/report.jsonとしてリポジトリに保存し、戦略ソースと並べてコミットする。PRが意図的に戦略の動作を変更する場合、作成者はPRの一部としてベースラインを更新する。その差分が、何がなぜ変わったかの永続的なレコードになる。

同じワークフローは、GitLab CIやBitbucket Pipelines、Dockerが動く任意のランナーでも、まったく同じように動きます。YAMLの構文は書き換え、シェルコマンドはそのまま使います。

終了コードとパリティの検証

PineForgeランタイムは正常実行で終了コード0を返し、失敗時には0以外の終了コードを返します。入力ファイルがない場合は2、コンパイルの失敗は3、バックテストの失敗は4、トランスパイルの失敗(未対応のPineや構文エラー)は5です。CIは特別な処理をしなくても、これらの終了コードを検知します。

結果のチェック方法はワークフロー側で決めます。レポートは次のような形式です(抜粋):

{
  "engine": "pineforge",
  ...
  "summary": {
    "total_trades": 3483,
    "wins": 1288,
    "losses": 2194,
    "win_rate_pct": 36.9796,
    "net_pnl": 2518.39,
    "max_drawdown": -1382.76,
    ...
  },
  "trades": [
    {
      "n": 1,
      "side": "long",
      "entry_time": 1729402200000,
      "exit_time": 1729422000000,
      "entry_price": 2640.96,
      "exit_price": 2645.15,
      "qty": 1.0,
      "pnl": 4.19,
      "entry_bar_index": 34,
      "exit_bar_index": 56,
      ...
    }
  ],
  "metrics": { "all": { "profit_factor": 1.06, ... }, "equity": { "sharpe_tv": ..., ... } },
  "equity_curve": [ ... ],
  "fingerprint": { "digest": "sha256:...", ... }
}

簡単な数値パリティチェックにはsummary.net_pnlとsummary.total_tradesをベースラインと比較します。上のワークフローでは、trades配列も読み取ります。各取引の売買方向、エントリーバー、決済バーが変わっていないことが条件です。チェックに失敗すると、ワークフローは2つの取引一覧を1行に1取引ずつ並べてdiffを取ります。そのため、diffの行番号がそのまま取引番号になり、各行には取引の売買方向、バー、価格、数量、損益が変更前と変更後で表示されます。

数値フィールドの実用的な閾値:0.01(純損益の1セント)までの浮動小数点の差分を許容し、それより大きい差分は失敗とします。取引には許容範囲を設けません。上のワークフローは、売買方向、entry_bar_indexやexit_bar_indexのいずれかが変われば、損益にかかわらず失敗します。取引が別のバーにずれているなら、シグナルロジックが変わったということです。

実践的なパターンとして、ベースラインの更新を、PRラベルで制御する別のワークフローステップで行う方法があります。こうすると、意図的な変更が、その原因となったPineの変更と並んでコードレビューに表示されます。

次のステップ