跳到正文

引擎 CI:311/312 套 Pine 策略优秀

其余一套为已声明的异常,以琥珀色标出。

复合策略指标订单与风控周期与交易时段K 线内成交Pine 语言分析器检查
复合策略指标订单与风控周期与交易时段K 线内成交Pine 语言分析器检查

点击或拖动图表,查看任一点的数值。

来源:引擎 main 分支的公开测试集一致性检查,2026-09-29。

以表格形式查看图表数据

公开测试集一致性检查(引擎 main 分支,2026-09-29):312 套策略中 311 套为优秀;剩下的 1 套是已声明的异常。

分组策略优秀已声明的异常
复合策略53530
指标79790
订单与风控77770
周期与交易时段28280
K 线内成交880
Pine 语言56560
分析器检查11101

PineForge 在 CI 中回测你的 Pine Script,交易一旦偏离基线,构建即失败。

为何 CI 里也要 parity

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

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

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

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

PineForge 之所以适合这里,是因为运行时就是一个 Docker 镜像:能跑 Docker 的地方就能跑它,相同输入得到相同交易,并输出可用 jq 解析的 JSON 报告。回测本身不需要浏览器、不需要登录,也没有速率限制。

GitHub Actions 样板

完整的工作流:检出你的策略,用 pineforge-release 镜像把它转译成 C++,在你固定版本的 OHLCV CSV 上跑回测,再用jq解析 JSON 报告,并与已提交的基线对比;只要有一笔交易的方向或所在 K 线发生变化,或者净盈亏偏差超过你设定的阈值,就让构建失败。

运行时是个容器镜像,没有 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@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 语法,shell 命令保持不变。

退出码与 parity 断言

PineForge 运行时正常结束时退出码为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数组:每笔交易的方向、入场 K 线和出场 K 线都必须保持不变。检查未通过时,工作流会把两份交易列表按一行一笔交易输出,再用 diff 比对,因此 diff 的行号就是交易序号,每行列出一笔交易在改动前后的方向、K 线、价格、数量和盈亏。

数值字段的实用阈值:浮点差容忍到0.01(即净盈亏 1 美分),超出即失败。交易本身不设容差:上面的工作流对方向、entry_bar_index或exit_bar_index的任何变化都会判定失败,与盈亏无关。成交落在不同 K 线意味着信号逻辑已经变了。

一个实用的做法:把基线更新放进单独的工作流步骤,并以 PR 标签作为触发条件,这样有意为之的变更就会在代码评审中,和引发它的 Pine 改动一起出现。

下一步