PineForge backteste votre Pine Script en CI ; si les trades dérivent, le build échoue.
Pourquoi la parité en CI
Les stratégies dérivent comme n’importe quel code. Un paramètre renommé, une fonction utilitaire refactorisée, un changement d’API de Pine v6 passé inaperçu : chacun peut décaler vos signaux en silence, sans faire échouer le moindre test existant. Sans contrôle de parité en CI, un commit vieux de deux mois peut modifier la liste de trades de la semaine dernière, et vous ne le découvrirez que le jour où le P&L en réel semblera anormal.
La discipline qui consiste à commiter votre liste de trades de référence aux côtés du code source de votre stratégie est celle-là même qui rend les logiciels fiables : vous décrivez la sortie attendue, et le build échoue si la sortie réelle diverge. Pour un backtest, la sortie attendue est la liste de trades (barre d’entrée, barre de sortie, sens, taille, prix d’exécution) sur un jeu de données historique épinglé.
Quand cette comparaison tourne à chaque commit, vous obtenez un mécanisme à sens unique : le comportement historique de votre stratégie ne peut que s’améliorer, jamais régresser par accident. Vous savez exactement quel commit a modifié la sortie, parce que le build a échoué sur ce commit. Vous pouvez faire un git blame sur votre courbe d’équité.
C’est plus important que la plupart des quants ne le réalisent au départ. L’écart entre « j’ai testé cette stratégie » et « j’ai un enregistrement reproductible et versionné de chaque signal historique que cette stratégie a jamais produit » est énorme. Le premier est une capture d’écran. Le second est une piste d’audit. La CI est la façon de maintenir le second sans effort manuel à chaque push.
PineForge s’y prête parce que le runtime est une image Docker : il tourne partout où Docker tourne, donne les mêmes trades pour des données d’entrée identiques et écrit un rapport JSON que vous pouvez analyser avec jq. Pas de navigateur, pas de connexion, pas de limite de débit sur le backtest lui-même.
Exemple GitHub Actions
Le workflow complet : récupérez votre stratégie, transpilez-la en C++ avec l’image pineforge-release, lancez le backtest sur votre CSV OHLCV épinglé, analysez le rapport JSON avec jq, comparez-le à votre baseline commitée, et faites échouer le build si un trade change de sens ou de barre, ou si le delta de PnL net dépasse votre seuil.
Le runtime est une image de conteneur, sans clé d’API à gérer. Il suffit d’un docker run dans le job. Créez .github/workflows/backtest.yml :
name: Backtest parity
on:
push:
branches: [main, "feat/**"]
pull_request:
jobs:
backtest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Pin the image version so results only change when you change it.
- 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-release:1.10.0 > 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-release:1.10.0 \
> report.json
- name: Assert parity
run: |
# Every trade keeps its side and its entry and exit bars,
# and net P&L moves by at most THRESHOLD.
BARS='[.trades[] | [.side, .entry_bar_index, .exit_bar_index]]'
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 [ "$(jq -c "$BARS" report.json)" != "$(jq -c "$BARS" baseline/report.json)" ]; then
echo "Parity drift: a trade changed side, entry bar or exit bar"
elif awk "BEGIN{exit !($DELTA > $THRESHOLD)}"; then
echo "Parity drift: net_pnl delta $DELTA exceeds threshold $THRESHOLD"
else
echo "Parity OK - same sides and bars, net_pnl delta $DELTA (threshold $THRESHOLD)"
exit 0
fi
# One trade per line, so diff's line numbers are trade numbers.
TRADE='.trades[] | {side, entry_bar_index, exit_bar_index, entry_price, exit_price, qty, pnl}'
diff <(jq -c "$TRADE" baseline/report.json) <(jq -c "$TRADE" report.json)
exit 1Stockez votre rapport de baseline dans baseline/report.json dans le dépôt, commité aux côtés du code source de la stratégie. Quand une PR modifie intentionnellement le comportement de la stratégie, l’auteur met à jour la baseline dans le cadre de la PR. Ce diff devient un enregistrement permanent de ce qui a changé et pourquoi.
Le même workflow s’exécute à l’identique sur GitLab CI, Bitbucket Pipelines ou tout runner compatible avec Docker. Changez la syntaxe YAML, gardez les commandes shell.
Codes de sortie et assertion de parité
Le runtime PineForge se termine avec le code 0 en cas d’exécution sans erreur, et avec un code non nul en cas d’échec : 2 si un fichier d’entrée manque, 3 en cas d’échec de compilation, 4 en cas d’échec du backtest et 5 en cas d’échec de transpilation (Pine non pris en charge ou erreur de syntaxe). La CI les détecte sans traitement particulier.
La vérification du résultat relève de votre workflow. Le rapport ressemble à ceci (extrait) :
{
"engine": "pineforge",
...
"summary": {
"total_trades": 3483,
"wins": 1288,
"losses": 2194,
"win_rate_pct": 36.9796,
"net_pnl": 2518.39,
"max_drawdown": -1382.76,
...
},
"trades": [
{
"n": 1,
"side": "long",
"entry_time": 1729402200000,
"exit_time": 1729422000000,
"entry_price": 2640.96,
"exit_price": 2645.15,
"qty": 1.0,
"pnl": 4.19,
"entry_bar_index": 34,
"exit_bar_index": 56,
...
}
],
"metrics": { "all": { "profit_factor": 1.06, ... }, "equity": { "sharpe_tv": ..., ... } },
"equity_curve": [ ... ],
"fingerprint": { "digest": "sha256:...", ... }
}Pour une vérification numérique de parité simple, comparez summary.net_pnl et summary.total_trades à la baseline. Le workflow ci-dessus lit aussi le tableau trades : chaque trade doit conserver son sens ainsi que ses barres d’entrée et de sortie. Quand une vérification échoue, le workflow compare les deux listes de trades avec diff, à raison d’un trade par ligne : les numéros de ligne de diff sont donc les numéros des trades, et chaque ligne montre le sens, les barres, les prix, la quantité et le P&L d’un trade, avant et après.
Un seuil pratique pour les champs numériques : acceptez les deltas en virgule flottante jusqu’à 0.01 (un centime de P&L net) et échouez sur tout écart supérieur. Pour les trades, aucune tolérance : le workflow ci-dessus échoue dès que le sens, entry_bar_index ou exit_bar_index d’un trade diffère, quel que soit le P&L. Un trade qui tombe sur une autre barre signifie que votre logique de signal a changé.
Un schéma pratique : mettez à jour la baseline dans une étape de workflow distincte, conditionnée à un label de PR, pour qu’un changement intentionnel apparaisse en revue de code à côté de la modification Pine qui l’a causé.