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 改动一起出现。