CI · GitHub Actions

把 PineScript 回测嵌进 CI(GitHub Actions / GitLab / 任意 Docker 运行器)

策略仓库每次推送自动跑批;parity 断言失败即构建失败;镜像与本地完全一致。

为何 CI 里也要 parity

策略会像业务代码一样漂移:重命名、抽函数、漏读的 v6 API —— 单测绿灯也可能悄悄改信号。没有 parity 门禁,你不知道两个月前的提交何时改了上周的成交列表。

把「期望成交列表」与源码一同入库,与可靠软件一样:期望输出不一致就让构建失败。

每次提交都 diff,就形成棘轮:历史行为只会向好演化;你能 git blame 到 exactly 打破了权益曲线。

「我跑过一次回测」≠「我有一份版本化的信号流水」;后者才是审计资产;CI 负责默默保管。

Docker 镜像 + 确定性 JSON 结构约定 + 无浏览器 —— PineForge 天生适配各类运行器。

GitHub Actions 样板

完整流水线:检出你的策略,用运行时容器把它转译成 C++,对钉死的 OHLCV CSV 跑回测,再用 jq解析 JSON → 与已提交的基线对比 → 净盈亏偏差超阈值就让构建失败。

运行时是个容器镜像,没有 API 密钥要管 —— 在 job 里 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 语法,shell 命令保持不变。

退出码与 parity 断言

PineForge 运行时正常结束时退出码为 0;若 Pine 源码非法、遇到不支持的内置、或数据文件解析失败则返回非零 —— CI 系统会自动拾取这些退出码,引擎级故障无需额外判定逻辑。

parity 断言属于工作流自己的职责。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数组 —— 入/出场 K 线索引、方向、成交价都应一致。不一致时,比对输出会精确指出哪一笔成交、哪一根 K 线、价差多少。

数值字段的实用阈值:浮点差容忍到 0.01(一美分,或归一序列上的一个 bp),超出即失败。逐笔比对时,只要 entry_bar exit_bar不一致就应硬失败 —— 与盈亏是否接近无关;成交落在不同 K 线意味着信号逻辑已经变了。

实践上常把基线更新拆成独立工作流,并用 PR 标签触发;这样有意的策略升级会在 PR 时间线留下基线差异,便于代码评审对齐 Pine 变更。

资源