Chạy backtest PineScript trong CI (GitHub Actions, GitLab, mọi nơi có Docker)
Mỗi commit vào repo strategy trigger backtest. Parity được assert, regression fail build. Cùng image Docker bạn chạy local, execute trong runner của bạn.
Vì sao parity trong CI
Strategy drift như mọi code khác. Một lần rename tham số, một lần refactor helper function, một thay đổi API Pine v6 bạn không để ý — bất kỳ cái nào cũng có thể âm thầm shift signal mà không phá test hiện có. Không có parity gate trong CI, một commit hai tháng trước có thể đổi trade list tuần trước và bạn chỉ phát hiện khi P&L live trông sai.
Kỷ luật commit reference trade list cùng strategy source là cùng kỷ luật làm software đáng tin: bạn mô tả output kỳ vọng, build fail nếu output thực lệch. Với backtest, output kỳ vọng là trade list — entry bar, exit bar, hướng, size, fill price — đối chiếu với dataset lịch sử đã pin.
Khi so sánh đó chạy mỗi commit, bạn có một ratchet: hành vi lịch sử của strategy chỉ tăng tiến, không vô tình regress. Bạn biết exactly commit nào đổi output, vì build fail ở commit đó. Bạn có git blame cho equity curve.
Điều này quan trọng hơn đa số quant nhận ra ban đầu. Khoảng cách giữa "tôi đã test strategy này" và "tôi có một bản ghi version-controlled, reproducible của mọi signal lịch sử strategy này từng sinh ra" là khổng lồ. Cái đầu là screenshot. Cái sau là audit trail. CI là cách bạn duy trì cái sau mà không phải làm thủ công mỗi lần push.
PineForge cho phép điều này với strategy Pine vì runtime là Docker image: chạy mọi nơi Docker chạy, sinh output deterministic với cùng input, và trả schema JSON stable parse được trong shell script. Không browser, không auth flow, không rate limit ở chính bước backtest.
Ví dụ GitHub Actions
Workflow đầy đủ: checkout strategy, transpile sang shared object qua codegen API, chạy backtest với OHLCV CSV đã pin, parse JSON report bằng jq, so với baseline đã commit, và fail build nếu net PnL delta vượt threshold của bạn.
Thêm PineForge API key làm repository secret tên PINEFORGE_API_KEY, rồi tạo .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 .so
run: |
curl -s https://api.pineforge.io/v1/codegen \
-H "Authorization: Bearer ${{ secrets.PINEFORGE_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"source": "'"$(cat strategy.pine | jq -Rs .)"'"}' \
| jq -r '.artifact_url' \
| xargs curl -sL -o strategy.so
- name: Pull PineForge runtime
run: docker pull ghcr.io/pineforge/runtime:latest
- name: Run backtest
run: |
docker run --rm \
-v "$PWD/strategy.so":/strategy.so \
-v "$PWD/data/ohlcv.csv":/data.csv \
ghcr.io/pineforge/runtime:latest \
run /strategy.so --data /data.csv --output json \
> 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)"Lưu baseline report ở baseline/report.jsontrong repo, commit cùng strategy source. Khi PR đổi hành vi strategy có chủ đích, author update baseline trong PR. Diff đó trở thành bản ghi vĩnh viễn về cái gì đã đổi và vì sao.
Cùng workflow chạy y hệt trên GitLab CI, Bitbucket Pipelines, hay runner nào support Docker — đổi syntax YAML, giữ shell command.
Exit code và assert parity
Runtime PineForge exit với code 0khi chạy sạch và non-zero khi engine error (Pine sai cú pháp, built-in chưa support, parse data file fail). CI tự động bắt exit code — không cần xử lý đặc biệt cho lỗi engine.
Assert parity là chuyện riêng, làm trong workflow. Schema JSON report stable across patch version:
{
"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"
}
]
}Cho check parity số đơn giản, so summary.net_pnl và summary.total_trades với baseline. Cho strict trade-for-trade parity, diff full tradesarray — bar index entry/exit, hướng, fill price phải khớp hết. Khi không khớp, output diff chỉ đúng lệnh nào lệch, ở bar nào, fill price chênh bao nhiêu.
Threshold thực dụng cho field số: chấp nhận delta floating-point tới 0.01(một cent, hay một basis point trên series chuẩn hóa) và fail nếu lớn hơn. Cho diff trade-for-trade, mọi mismatch ở entry_bar hoặc exit_barluôn là hard failure bất kể PnL gần đến đâu — lệnh rơi vào bar khác nghĩa là logic signal đã đổi.
Team adopt parity CI thường chạy update baseline làm step workflow riêng, gate bằng PR label. Như vậy strategy improve có chủ đích được document trong PR timeline — diff baseline xuất hiện trong code review cùng thay đổi Pine gây ra nó.