CI · GitHub Actions

Backtests PineScript dans votre CI (GitHub Actions, GitLab, tout runner Docker)

Chaque push rejoue la stratégie : parité assertée, régression = build rouge. Même image Docker qu'en local, orchestrée par votre runner.

Pourquoi la parité en CI

Les stratégies dérivent comme tout code. Un renommage de paramètre, un refactoring d'une fonction utilitaire, un changement d'API Pine v6 que vous n'avez pas remarqué — chacun de ces éléments peut décaler silencieusement des signaux sans casser aucun test existant. Sans gate de parité en CI, un commit d'il y a deux mois peut modifier la liste de transactions de la semaine dernière, et vous ne le découvrirez que quand le PnL live semblera anormal.

La discipline qui consiste à commiter votre liste de transactions de référence aux côtés du code source de votre stratégie est la même que celle qui rend les logiciels fiables : vous décrivez la sortie attendue, et votre build échoue si la sortie réelle diverge. Pour un backtest, la sortie attendue est la liste de transactions — barre d'entrée, barre de sortie, direction, taille, prix d'exécution — contre 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 exactly 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 rend cela possible pour les stratégies Pine parce que le runtime est une image Docker : il tourne partout où Docker tourne, il produit une sortie déterministe pour des entrées identiques, et il renvoie un schéma JSON stable que l'on peut parser dans des scripts shell. Pas de navigateur, pas de flux d'authentification, pas de limite de débit sur l'exécution du backtest lui-même.

Exemple GitHub Actions

Le workflow complet : récupérez votre stratégie, transpilez-la en C++ avec le conteneur runtime, lancez le backtest sur votre CSV OHLCV épinglé, parsez le rapport JSON avec jq, comparez à votre baseline commitée, et faites échouer le build si le delta de PnL net dépasse votre seuil.

Le runtime est une image conteneur : aucune clé d'API à gérer — il suffit de faire 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@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)"

Stockez votre rapport de baseline dans baseline/report.jsondans 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 de manière identique sur GitLab CI, Bitbucket Pipelines ou tout runner supportant Docker — changez la syntaxe YAML, gardez les commandes shell.

Codes de sortie et assertion de parité

Le runtime PineForge se termine avec le code 0en cas d'exécution propre et avec un code non nul pour toute erreur moteur (Pine malformé, built-in non supporté, échec de parsing du fichier de données). Les systèmes CI récupèrent ces codes de sortie automatiquement — aucune gestion spéciale n'est nécessaire pour les erreurs au niveau du moteur.

L'assertion de parité est une préoccupation distincte, implémentée dans votre workflow. Le schéma du rapport JSON est stable entre les versions patch :

{
  "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"
    }
  ]
}

Pour une vérification numérique de parité simple, comparez summary.net_pnl et summary.total_trades à la baseline. Pour une parité stricte transaction par transaction, diffez le tableau tradescomplet — les indices de barres d'entrée et de sortie, les directions et les prix d'exécution doivent tous correspondre. Lorsqu'ils ne correspondent pas, la sortie du diff localise exactement quelle transaction a divergé, sur quelle barre et quelle était la différence de prix.

Un seuil pratique pour les champs numériques : acceptez les deltas en virgule flottante jusqu'à 0.01(un centime, ou un point de base sur une série normalisée) et échouez sur tout écart supérieur. Pour les diffs transaction par transaction, tout écart dans entry_bar ou exit_barest toujours un échec dur, quelle que soit la proximité du PnL — une transaction tombant sur une barre différente signifie que votre logique de signal a changé.

Les équipes qui adoptent la CI de parité exécutent généralement la mise à jour de la baseline comme étape de workflow distincte, conditionnée à un label de PR. Ainsi, une amélioration intentionnelle de la stratégie est documentée dans le fil de la PR — le diff de baseline apparaît dans la revue de code aux côtés du changement Pine qui l'a causé.

Pour commencer