pineforge
CI / GitHub Actions

PineScript-Backtests in CI fahren (GitHub Actions, GitLab, alles mit Docker)

Jeder Commit ins Strategie-Repo triggert einen Backtest. Parität wird asserted, Regressionen brechen den Build. Dasselbe Docker-Image, das du lokal fährst, läuft in deinem Runner.

Warum Parität in CI

Strategien driften wie jeder Code. Ein Parameter-Rename, ein Refactor einer Helper-Funktion, ein Pine-v6-API-Change, den du nicht mitbekommen hast — alles davon kann Signale leise verschieben, ohne einen einzigen bestehenden Test zu brechen. Ohne Parity-Gate in CI kann ein Commit von vor zwei Monaten die Trade-Liste der letzten Woche verändern — und du merkst es erst, wenn das Live-P&L falsch aussieht.

Die Disziplin, deine Referenz-Trade-Liste neben den Strategie-Source zu committen, ist dieselbe Disziplin, die Software zuverlässig macht: Du beschreibst den erwarteten Output, und dein Build bricht, wenn der tatsächliche Output abweicht. Bei einem Backtest ist der erwartete Output die Trade-Liste — Entry-Bar, Exit-Bar, Richtung, Size, Fill-Preis — gegen ein gepinntes historisches Dataset.

Wenn dieser Vergleich auf jedem Commit läuft, bekommst du eine Sperrklinke: Das historische Verhalten deiner Strategie kann nur besser werden, nie versehentlich regredieren. Du weißt exactly , welcher Commit den Output verändert hat, weil der Build auf diesem Commit gebrochen ist. Du hast einen git blame für deine Equity-Kurve.

Das zählt mehr, als die meisten Quants anfangs wahrhaben wollen. Die Lücke zwischen „Ich habe diese Strategie getestet“ und „Ich habe einen reproduzierbaren, versionierten Record jedes historischen Signals, das diese Strategie je erzeugt hat“ ist riesig. Das Erste ist ein Screenshot. Das Zweite ist ein Audit-Trail. CI ist die Art, wie du das Zweite ohne manuellen Aufwand auf jedem Push pflegst.

PineForge macht das für Pine-Strategien möglich, weil die Runtime ein Docker-Image ist: Es läuft überall, wo Docker läuft, produziert deterministischen Output bei gleichem Input und liefert ein stabiles JSON-Schema, das in Shell-Skripten sicher zu parsen ist. Kein Browser, kein Auth-Flow, kein Rate-Limit auf der Backtest-Execution selbst.

GitHub-Actions-Beispiel

Der vollständige Workflow: Strategie auschecken, via Codegen-API zu einem Shared Object transpilieren, Backtest gegen deine gepinnte OHLCV-CSV fahren, JSON-Report parsen mit jq, gegen deine commitete Baseline vergleichen, Build brechen, wenn das Net-P&L-Delta deinen Schwellenwert überschreitet.

Trag deinen PineForge-API-Key als Repository-Secret namens PINEFORGE_API_KEYein, dann lege .github/workflows/backtest.ymlan:

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)"

Lege deinen Baseline-Report unter baseline/report.jsonab und committe ihn neben dem Strategie-Source. Wenn ein PR das Strategie-Verhalten beabsichtigt verändert, aktualisiert der Author die Baseline als Teil des PRs. Dieser Diff wird zum permanenten Record, was sich geändert hat und warum.

Derselbe Workflow läuft identisch auf GitLab CI, Bitbucket Pipelines oder einem beliebigen Runner mit Docker-Support — YAML-Syntax tauschen, Shell-Befehle bleiben.

Exit-Codes und Parität asserten

Die PineForge-Runtime exitet mit Code 0bei einem sauberen Lauf und non-zero bei jedem Engine-Fehler (malformed Pine, nicht unterstützter Built-in, Datei-Parse-Fehler). CI-Systeme picken diese Exit-Codes automatisch auf — kein Special-Handling für Engine-Level-Failures nötig.

Parity-Assertion ist eine separate Sache und liegt in deinem Workflow. Das JSON-Report-Schema ist über Patch-Versionen stabil:

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

Für einen einfachen numerischen Parity-Check vergleiche summary.net_pnl und summary.total_trades gegen die Baseline. Für strikte Trade-für-Trade-Parität diffe das vollständige trades-Array — Entry- und Exit-Bar-Indizes, Richtungen und Fill-Preise sollten alle matchen. Wenn nicht, zeigt der Diff exakt, welcher Trade abgewichen ist, auf welcher Bar und mit welcher Fill-Preis-Differenz.

Praktischer Schwellenwert für numerische Felder: Floating-Point-Deltas bis 0.01akzeptieren (ein Cent oder ein Basispunkt auf einer normalisierten Serie) und auf alles Größere brechen. Bei Trade-für-Trade-Diffs ist jede Mismatch in entry_bar oder exit_barimmer ein hartes Failure, unabhängig von P&L-Nähe — ein Trade auf einer anderen Bar bedeutet, dass deine Signal-Logik sich geändert hat.

Teams, die Parity-CI einführen, fahren das Baseline-Update typischerweise als separaten Workflow-Step, der an ein PR-Label gekoppelt ist. So landet eine beabsichtigte Strategie-Verbesserung im PR-Timeline dokumentiert — der Baseline-Diff taucht im Code-Review neben der Pine-Änderung auf, die ihn verursacht hat.

Loslegen