把 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 变更。