Rode backtests de PineScript no CI (GitHub Actions, GitLab, qualquer coisa Docker)
Cada commit no repo da sua estratégia dispara um backtest. Paridade é asserida, regressões quebram o build. Mesma imagem Docker que você roda local, executada no seu runner.
Por que paridade no CI
Estratégias driftam como qualquer código. Um rename de parâmetro, uma refatoração de função helper, uma mudança na API do Pine v6 que você não percebeu — qualquer um pode mexer silenciosamente nos sinais sem quebrar nenhum teste existente. Sem um gate de paridade no CI, um commit que você fez dois meses atrás pode mudar a trade-list da semana passada e você só vai descobrir quando o P&L live ficar errado.
A disciplina de commitar sua trade-list de referência junto com o source da estratégia é a mesma disciplina que faz software ser confiável: você descreve o output esperado e seu build quebra se o output real divergir. Pra um backtest, o output esperado é a trade-list — barra de entrada, barra de saída, direção, tamanho, preço de fill — contra um dataset histórico pinado.
Quando essa comparação roda em todo commit, você ganha uma catraca: o comportamento histórico da sua estratégia só pode melhorar, nunca regredir por acidente. Você sabe exactly qual commit mudou o output, porque o build quebrou nesse commit. Você tem um git blame pra sua curva de equity.
Isso importa mais do que a maioria dos quants percebe no começo. O abismo entre "testei essa estratégia" e "tenho um registro reproduzível, versionado, de cada sinal histórico que essa estratégia já produziu" é enorme. O primeiro é um print. O segundo é um audit trail. CI é como você mantém o segundo sem esforço manual em cada push.
PineForge faz isso ser possível pra estratégias Pine porque o runtime é uma imagem Docker: roda em qualquer lugar que rode Docker, produz output determinístico dado o mesmo input e devolve um schema JSON estável que é seguro de parsear em scripts shell. Não tem browser, não tem fluxo de autenticação, não tem rate limit na execução do backtest em si.
Exemplo em GitHub Actions
O fluxo completo: faz checkout da sua estratégia, transpila pra um shared object via API de codegen, roda o backtest contra seu CSV de OHLCV pinado, parseia o relatório JSON com jq, compara com sua baseline commitada e quebra o build se o delta de PnL líquido passar do seu threshold.
Adicione sua API key do PineForge como repository secret chamado PINEFORGE_API_KEY, depois crie .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)"Guarde seu relatório baseline em baseline/report.jsonno repo, commitado junto com o source da estratégia. Quando um PR muda o comportamento da estratégia de propósito, o autor atualiza a baseline como parte do PR. Esse diff vira um registro permanente do que mudou e por quê.
O mesmo workflow roda igual no GitLab CI, Bitbucket Pipelines ou qualquer runner que suporte Docker — troca a sintaxe YAML, mantém os comandos shell.
Exit codes e asserção de paridade
O runtime do PineForge sai com código 0num run limpo e diferente de zero em qualquer erro de engine (Pine malformado, built-in não suportado, falha de parse no arquivo de dados). Sistemas de CI pegam esses exit codes automaticamente — sem tratamento especial pra falhas de engine.
Asserção de paridade é uma preocupação separada, implementada no seu workflow. O schema do relatório JSON é estável entre patch versions:
{
"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"
}
]
}Pra um check numérico simples de paridade, compare summary.net_pnl e summary.total_trades contra a baseline. Pra paridade estrita trade a trade, faça diff do array trades inteiro — índices de barra de entrada e saída, direções e preços de fill devem todos bater. Quando não batem, o output do diff aponta exatamente qual trade divergiu, em que barra e qual a diferença de preço de fill.
Threshold prático pra campos numéricos: aceite deltas de ponto flutuante até 0.01(um centavo, ou um basis point numa série normalizada) e quebre em qualquer coisa maior. Pra diffs trade a trade, qualquer mismatch em entry_bar ou exit_baré sempre falha hard, independente da proximidade de PnL — uma trade caindo numa barra diferente significa que a sua lógica de sinal mudou.
Times que adotam paridade no CI normalmente rodam o update da baseline como um step separado de workflow gateado por uma label de PR. Assim uma melhoria intencional na estratégia fica documentada no timeline do PR — o diff da baseline aparece no code review junto com a mudança no Pine que causou.