pineforge
CI / GitHub Actions

Esegui backtest PineScript in CI (GitHub Actions, GitLab, qualsiasi cosa con Docker)

Ogni commit alla tua repo di strategia fa partire un backtest. La parità viene asserita, le regressioni fanno fallire la build. Stessa immagine Docker che fai girare in locale, eseguita nel tuo runner.

Perché la parità in CI

Le strategie driftano come qualunque codice. Un rinominare di parametro, un refactor di una funzione helper, un cambio di API in Pine v6 che non hai notato — qualsiasi di questi può spostare i segnali in silenzio senza rompere alcun test esistente. Senza un gate di parità in CI, un commit di due mesi fa può cambiare la trade list della scorsa settimana e te ne accorgi solo quando il P&L live sembra sbagliato.

La disciplina di committare la trade list di riferimento accanto al sorgente della strategia è la stessa disciplina che rende affidabile il software: descrivi l'output atteso e la build fallisce se l'output reale diverge. Per un backtest, l'output atteso è la trade list — barra di entry, barra di exit, direzione, size, prezzo di fill — contro un dataset storico pinnato.

Quando quel confronto gira a ogni commit ottieni un cricchetto: il comportamento storico della tua strategia può solo migliorare, mai regredire per sbaglio. Sai exactly quale commit ha cambiato l'output, perché la build è fallita su quel commit. Hai un git blame per la tua curva di equity.

Questo conta più di quanto la maggior parte dei quant si renda conto all'inizio. Il gap fra «ho testato questa strategia» e «ho un record version-controlled e riproducibile di ogni segnale storico che questa strategia abbia mai prodotto» è enorme. Il primo è uno screenshot. Il secondo è un audit trail. La CI è come mantieni il secondo senza sforzo manuale a ogni push.

PineForge rende tutto questo possibile per le strategie Pine perché il runtime è un'immagine Docker: gira ovunque giri Docker, produce output deterministico dati gli stessi input e restituisce uno schema JSON stabile e sicuro da parsare in shell script. Niente browser, niente flow di autenticazione, nessun rate limit sull'esecuzione del backtest in sé.

Esempio GitHub Actions

Il workflow completo: checkout della tua strategia, transpile in shared object via API codegen, esecuzione del backtest contro il tuo CSV OHLCV pinnato, parsing del report JSON con jq, confronto contro la baseline committata e fallimento della build se il delta di net PnL eccede la tua soglia.

Aggiungi la tua API key PineForge come repository secret di nome PINEFORGE_API_KEY, poi 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 .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)"

Salva il report di baseline in baseline/report.json nel repository, committato accanto al sorgente della strategia. Quando una PR cambia intenzionalmente il comportamento della strategia, l'autore aggiorna la baseline come parte della PR. Quel diff diventa un record permanente di cosa è cambiato e perché.

Lo stesso workflow gira identico su GitLab CI, Bitbucket Pipelines o qualunque runner che supporti Docker — cambia la sintassi YAML, tieni gli stessi comandi shell.

Codici di uscita e asserzione di parità

Il runtime PineForge esce con codice 0 in caso di run pulita e non-zero su qualunque errore di engine (Pine malformato, built-in non supportata, fallimento di parsing del file dati). I sistemi CI raccolgono questi exit code in automatico — nessun handling speciale serve per i fallimenti a livello engine.

L'asserzione di parità è una preoccupazione separata, implementata nel tuo workflow. Lo schema del report JSON è stabile fra le 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"
    }
  ]
}

Per un check di parità numerica semplice, confronta summary.net_pnl e summary.total_trades contro la baseline. Per la parità stretta trade per trade, fai diff dell'array trades completo — gli indici di barra di entry e exit, le direzioni e i prezzi di fill devono combaciare tutti. Quando non combaciano, il diff indica esattamente quale trade è divergente, su quale barra è successo e qual è la differenza di prezzo di fill.

Una soglia pratica per i campi numerici: accetta delta floating-point fino a 0.01(un centesimo, o un basis point su una serie normalizzata) e fai fallire qualsiasi cosa più grande. Per i diff trade per trade, qualunque mismatch su entry_bar o exit_bar è sempre un fallimento duro a prescindere dalla vicinanza del PnL — un trade che cade su una barra diversa significa che la tua logica di segnale è cambiata.

I team che adottano la parità in CI tipicamente fanno girare l'aggiornamento della baseline come step di workflow separato, gated su una label di PR. Così un miglioramento di strategia intenzionale viene documentato nella timeline della PR — il diff di baseline appare nella code review accanto al cambio Pine che lo ha causato.

Inizia