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 o checkout da sua estratégia, transpila pra C++ com o container do runtime, 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.
O runtime é uma imagem de container, então não tem API key pra gerenciar — é só dar docker run no job. 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 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)"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.
