Corre backtests PineScript en CI (GitHub Actions, GitLab, cualquier Docker)
Cada confirmación de cambio en tu repositorio de estrategias dispara un backtest. Se verifica la paridad; las regresiones hacen fallar la compilación. La misma imagen Docker que en local, ejecutada en tu runner.
Por qué paridad en CI
Las estrategias derivan como cualquier código. Renombrar un parámetro, refactorizar una función auxiliar, un cambio en la API de Pine v6 que pasó desapercibido: cualquiera puede desplazar señales en silencio sin romper pruebas. Sin una puerta de paridad en CI, un commit antiguo puede cambiar la lista de operaciones de la semana pasada y solo lo notas cuando el PnL en vivo «huele» mal.
Versionar junto al Pine tu lista de operaciones de referencia es la misma disciplina que hace fiable el software: defines la salida esperada y la compilación falla si diverge. En un backtest, la salida esperada es la lista de operaciones — barra de entrada y salida, dirección, tamaño y precio de ejecución — respecto a un histórico fijado.
Si esa comparación corre en cada commit, tienes un trinquete: el comportamiento histórico solo mejora, no regresa por accidente. Sabes exactly qué commit cambió la salida, porque la compilación falló en ese punto. Puedes usar git blame sobre tu curva de equity.
Esto pesa más de lo que muchos quants creen al principio. La brecha entre «probé esta estrategia» y «tengo un registro versionado de cada señal histórica» es enorme. Lo primero es una captura de pantalla; lo segundo es una pista de auditoría. La CI mantiene lo segundo sin esfuerzo manual en cada push.
PineForge lo hace posible porque el runtime es una imagen Docker: se ejecuta donde Docker está disponible, la salida es determinista con los mismos datos de entrada y el JSON es estable para analizarlo desde scripts. Sin navegador, sin flujo de autenticación y sin límite de cadencia en la ejecución del backtest.
Ejemplo GitHub Actions
El flujo completo: obtén una copia de tu estrategia, transpílala a C++ con el contenedor del runtime, ejecuta el backtest contra tu CSV OHLCV fijado y analiza el informe JSON con jq, compara contra la línea base incluida en el repositorio y haz fallar la compilación si el delta del PnL neto supera tu umbral.
El runtime es una imagen de contenedor, así que no hay ninguna API key que gestionar — basta con un docker run en el job. Crea .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)"Guarda la línea base en baseline/report.jsonen el repositorio, versionada junto al Pine. Si un PR cambia el comportamiento de forma deliberada, quien lo abre actualiza la línea base en el mismo PR. Ese diff queda como registro permanente del qué y el porqué.
El mismo flujo funciona igual en GitLab CI, Bitbucket Pipelines o cualquier runner con Docker: cambia la sintaxis YAML y conserva los comandos de shell.
Códigos de salida y comprobación de paridad
El runtime PineForge sale con código 0en ejecución limpia y distinto de cero ante error del motor (Pine mal formado, función integrada no admitida, fallo al analizar los datos). Los sistemas de CI recogen estos códigos de salida automáticamente, sin tratamiento especial para fallos del motor.
Comprobar la paridad es una cuestión aparte, implementada en tu flujo de trabajo. El esquema JSON del informe es estable entre versiones que solo incrementan el parche:
{
"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"
}
]
}Para una comprobación numérica sencilla, compara summary.net_pnl y summary.total_trades contra la línea base. Para paridad estricta operación por operación, haz diff del array completo de trades`trades` — los índices de barra de entrada y salida, las direcciones y los precios de ejecución deben coincidir todos. Si no, la salida del diff localiza qué operación divergió, en qué barra y cuánto difiere el precio de ejecución.
Umbral práctico en campos numéricos: acepta diferencias en coma flotante hasta 0.01(un centavo, o un punto básico en una serie normalizada) y haz fallar la compilación si se supera. En diffs operación por operación, cualquier discrepancia en entry_bar o exit_bares un fallo grave aunque el PnL coincida: si la operación cae en otra barra, cambió la lógica de la señal.
Los equipos que aplican paridad en CI suelen actualizar la línea base en un flujo de trabajo aparte condicionado a una etiqueta del PR. Así una mejora intencionada queda en la línea temporal del PR: el diff de la línea base aparece en la revisión junto al cambio en Pine.