CI에 패리티를 거는 이유
전략도 코드처럼 드리프트합니다. 파라미터 이름 변경, 헬퍼 리팩터, 놓친 Pine v6 API 변경 — 다 기존 테스트를 깨지 않고 시그널만 살짝 바꿀 수 있습니다. CI에 패리티 게이트가 없으면 두 달 전 커밋이 지난주 트레이드 목록을 바꿔도 라이브 P&L이 이상해질 때까지 모릅니다.
참조 트레이드 목록을 전략 소스와 함께 커밋하는 규율은 소프트웨어를 믿게 만드는 규율과 같습니다: 기대 출력을 적고 실제 출력이 어긋나면 빌드를 깹니다. 백테스트에선 기대 출력이 고정 과거 데이터에 대한 트레이드 목록 — 진입 바, 청산 바, 방향, 크기, 체결가 — 입니다.
그 비교가 매 커밋마다 돌면 래칫이 생깁니다 — 과거 동작은 좋아질 수는 있어도 실수로 나빠지진 않습니다. 출력을 바꾼 커밋을 exactly 압니다. 그 커밋에서 빌드가 깨졌으니까요. 에퀴티 커브에 대한 git blame이 생깁니다.
처음엔 잘 모르는 퀀트에게 특히 큽니다. "전략을 테스트했다"와 "이 전략이 낸 모든 과거 시그널의 재현 가능·버전관리 기록이 있다" 사이 간격은 엄청납니다. 전자는 스크린샷, 후자는 감사 추적입니다. CI는 매 푸시마다 손으로 안 하고 후자를 유지하는 방법입니다.
PineForge가 가능한 이유는 런타임이 Docker 이미지이기 때문입니다 — Docker 도는 곳이면 어디서나, 같은 입력이면 결정론적 출력, 쉘에서 파싱 안전한 안정 JSON 스키마. 브라우저 없음, 인증 플로 없음, 백테스트 실행 자체의 레이트 리밋 없음.
GitHub Actions 예시
전체 워크플로: 전략을 체크아웃하고, 런타임 컨테이너로 C++로 트랜스파일하고, 고정해 둔 OHLCV CSV로 백테스트를 돌린 뒤, JSON 리포트를 파싱합니다 — jq로 JSON 리포트 파싱 → 커밋된 베이스라인과 비교 → 순 PnL 델타가 임계를 넘으면 빌드 실패.
런타임은 컨테이너 이미지라 관리할 API key가 없습니다 — 잡에서 그냥 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@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)"베이스라인 리포트를 baseline/report.json에 두고 전략 소스와 함께 커밋합니다. PR이 의도적으로 동작을 바꾸면 작성자가 베이스라인을 PR 안에서 갱신합니다. 그 디프가 무엇이 왜 바뀌었는지 영구 기록이 됩니다.
같은 워크플로는 GitLab CI, Bitbucket Pipelines, Docker를 쓰는 러너면 동일하게 실행합니다 — YAML만 바꾸고 쉘 커맨드는 유지.
종료 코드와 패리티 검증
PineForge 런타임은 정상 실행 시 종료 코드 0를 내고, 엔진 오류면 0이 아닙니다(잘못된 Pine, 미지원 빌트인, 데이터 파싱 실패). CI가 종료 코드를 자동으로 집습니다 — 엔진 실패용 특별 처리 불필요.
패리티 검증은 별개 관심사로 워크플로 안에서 구현합니다. JSON 리포트 스키마는 패치 버전 간 안정적입니다:
{
"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"
}
]
}단순 수치 패리티는 summary.net_pnl 와 summary.total_trades 를 베이스라인과 비교합니다. 엄격 트레이드 단위 패리티는 전체 trades배열을 디프합니다 — 진입·청산 바 인덱스, 방향, 체결가가 모두 맞아야 합니다. 안 맞으면 디프가 어떤 트레이드가 어떤 바에서 얼마나 어긋났는지 집어줍니다.
수치 필드 실무 임계: 부동소수점 델타를 0.01(1센트, 또는 정규화 시계열에서 1bp)까지 허용하고 그 이상은 실패. 트레이드 단위 디프에선 entry_bar 나 exit_bar불일치는 PnL 근접과 무관하게 항상 하드 실패입니다 — 다른 바에 트레이드가 붙었다는 건 시그널 로직이 바뀌었다는 뜻입니다.
패리티 CI를 도입한 팀은 보통 베이스라인 업데이트를 PR 라벨로 게이트한 별도 워크플로 스텝으로 둡니다. 의도된 개선이 PR 타임라인에 남고 베이스라인 디프가 원인 된 Pine 변경과 함께 리뷰에 뜹니다.
