본문으로 건너뛰기

엔진 CI: Pine 스크립트 312개 중 311개 우수

앰버색으로 표시한 나머지 하나는 미리 명시한 이상 사례입니다.

복합 전략지표주문과 리스크타임프레임과 세션봉 내부 체결Pine 언어분석기 검사
복합 전략지표주문과 리스크타임프레임과 세션봉 내부 체결Pine 언어분석기 검사

차트를 탭하거나 드래그하면 원하는 지점의 값이 보입니다.

출처: 엔진 main 브랜치의 공개 코퍼스 패리티 검사, 2026-09-29.

차트를 표로 보기

엔진 main의 공개 코퍼스 패리티 검사(2026-09-29): 전략 312개 중 311개가 우수이고, 남은 하나는 이상 사례로 지정돼 있습니다.

그룹전략우수지정된 이상 사례
복합 전략53530
지표79790
주문과 리스크77770
타임프레임과 세션28280
봉 내부 체결880
Pine 언어56560
분석기 검사11101

PineForge는 CI에서 Pine Script를 백테스트하고, 트레이드가 달라지면 빌드를 실패시킵니다.

CI에 패리티를 거는 이유

전략도 여느 코드처럼 드리프트합니다. 파라미터 이름 변경, 헬퍼 함수 리팩터링, 미처 알아채지 못한 Pine v6 API 변경 중 무엇이든 기존 테스트를 하나도 깨지 않고 시그널을 조용히 바꿀 수 있습니다. CI에 패리티 게이트가 없으면 두 달 전에 한 커밋이 지난주 트레이드 목록을 바꿔 놓아도, 라이브 P&L이 이상해 보이고 나서야 알게 됩니다.

참조 트레이드 목록을 전략 소스와 함께 커밋하는 규율은 소프트웨어를 믿을 수 있게 만드는 규율과 같습니다. 기대 출력을 적어 두면, 실제 출력이 어긋날 때 빌드가 실패합니다. 백테스트에서 기대 출력은 고정해 둔 과거 데이터셋으로 돌린 트레이드 목록(진입 봉, 청산 봉, 방향, 수량, 체결가)입니다.

그 비교가 커밋마다 돌면 래칫이 생깁니다. 전략의 과거 동작은 좋아질 수는 있어도 실수로 나빠지지는 않습니다. 출력을 바꾼 커밋을 정확히 압니다. 그 커밋에서 빌드가 깨졌으니까요. 에퀴티 커브에 대한 git blame이 생깁니다.

처음엔 잘 모르는 퀀트에게 특히 큽니다. "전략을 테스트했다"와 "이 전략이 낸 모든 과거 시그널의 재현 가능·버전관리 기록이 있다" 사이 간격은 엄청납니다. 전자는 스크린샷, 후자는 감사 추적입니다. CI는 매 푸시마다 손으로 안 하고 후자를 유지하는 방법입니다.

PineForge가 여기에 잘 맞는 이유는 런타임이 Docker 이미지이기 때문입니다. Docker가 도는 곳이면 어디서든 돌아가고, 입력이 같으면 트레이드도 같으며, jq로 파싱할 수 있는 JSON 리포트를 출력합니다. 브라우저도, 로그인도, 백테스트 자체에 대한 호출 제한도 없습니다.

GitHub Actions 예시

전체 워크플로: 전략 체크아웃, pineforge-release 이미지로 C++ 트랜스파일, 고정해 둔 OHLCV CSV로 백테스트 실행, jq로 JSON 리포트 파싱, 커밋된 베이스라인과 비교, 트레이드 하나라도 방향이나 봉이 바뀌거나 순 PnL 델타가 임계를 넘으면 빌드 실패.

런타임이 컨테이너 이미지라서 관리할 API 키가 없습니다. 잡에서 docker run으로 실행하기만 하면 됩니다. 새 워크플로 파일 .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 1

베이스라인 리포트를 baseline/report.json에 두고 전략 소스와 함께 커밋합니다. PR이 의도적으로 동작을 바꾸면 작성자가 베이스라인을 PR 안에서 갱신합니다. 그 디프가 무엇이 왜 바뀌었는지 영구 기록이 됩니다.

같은 워크플로가 GitLab CI, Bitbucket Pipelines, 그 밖에 Docker를 지원하는 어떤 러너에서도 똑같이 돌아갑니다. YAML 문법만 바꾸고 셸 명령은 그대로 두세요.

종료 코드와 패리티 검증

PineForge 런타임은 정상 실행 시 종료 코드 0을 반환하고, 실패하면 0이 아닌 코드를 반환합니다. 입력 파일이 없으면 2, 컴파일에 실패하면 3, 백테스트에 실패하면 4, 트랜스파일에 실패하면(지원하지 않는 Pine 또는 문법 오류) 5입니다. CI는 별도 처리 없이 이 코드를 인식합니다.

결과를 어떻게 검증할지는 워크플로에 달려 있습니다. 리포트는 다음과 같습니다(일부 생략):

{
  "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:...", ... }
}

단순 수치 패리티는 summary.net_pnl와 summary.total_trades를 베이스라인과 비교합니다. 위 워크플로는 trades 배열도 읽습니다. 트레이드마다 방향과 진입 봉, 청산 봉이 그대로여야 합니다. 검사에 실패하면 워크플로가 두 트레이드 목록을 한 줄에 트레이드 하나씩 적어 디프합니다. 그래서 디프 결과의 줄 번호가 곧 트레이드 번호이고, 줄마다 트레이드의 방향, 봉, 가격, 수량, 손익을 변경 전과 후로 볼 수 있습니다.

수치 필드 실무 임계: 부동소수점 델타를 0.01 (순손익 1센트)까지 허용하고 이를 넘으면 실패. 트레이드에는 허용 오차가 없습니다. 위 워크플로는 손익과 상관없이 방향, entry_bar_index나 exit_bar_index가 하나라도 바뀌면 실패합니다. 트레이드가 다른 봉에 찍혔다는 건 시그널 로직이 바뀌었다는 뜻입니다.

실용적인 패턴 하나: 베이스라인 갱신을 PR 라벨로 제어하는 별도 워크플로 단계로 두면, 의도한 변경이 그 원인이 된 Pine 변경과 나란히 코드 리뷰에 드러납니다.

다음 단계