Гоните бэктесты PineScript в CI (GitHub Actions, GitLab, всё, где есть Docker)
Каждый коммит в репозиторий стратегии запускает бэктест. Parity проверяется, регрессии валят сборку. Тот же Docker-образ, что вы гоняете локально, исполняется в вашем runner'е.
Зачем parity в CI
Стратегии дрейфуют, как любой код. Переименование параметра, рефакторинг helper-функции, не замеченное изменение API в Pine v6 — любое из этого может тихо сместить сигналы, не сломав ни одного существующего теста. Без parity-гейта в CI коммит, который вы сделали два месяца назад, может изменить прошлонедельный список сделок — и узнаете вы об этом, только когда живой P&L посмотрит криво.
Дисциплина «коммитить эталонный список сделок рядом с исходником стратегии» — та же самая, что делает софт надёжным: вы описываете ожидаемый выход, и сборка падает, если фактический разойдётся. Для бэктеста ожидаемый выход — это список сделок: бар входа, бар выхода, направление, размер, цена fill'а — против запинненного исторического датасета.
Когда это сравнение бежит на каждом коммите, получается храповик: историческое поведение стратегии может только улучшаться, но не регрессировать случайно. Вы знаете exactly , какой коммит изменил выход — потому что сборка упала именно на нём. У вашей кривой капитала есть git blame.
Это важнее, чем большинство квантов осознаёт сразу. Зазор между «я оттестил эту стратегию» и «у меня есть воспроизводимая, версионированная запись каждого исторического сигнала, что эта стратегия когда-либо генерила» — огромный. Первое — скриншот. Второе — аудит-трейл. CI — это то, как вы поддерживаете второе без ручной работы на каждом push'е.
PineForge делает это возможным для Pine-стратегий, потому что runtime — это Docker-образ: бежит везде, где бежит Docker, выдаёт детерминированный выход на одинаковых входах и возвращает стабильную JSON-схему, безопасную для парсинга в shell-скриптах. Без браузера, без auth-флоу, без rate limit'ов на сам прогон бэктеста.
Пример GitHub Actions
Полный воркфлоу: чекаут стратегии, транспиляция в shared object через codegen API, прогон бэктеста на запинненном OHLCV CSV, парсинг JSON-отчёта через jq, сравнение с прокомиченным baseline и падение сборки, если delta net PnL превышает порог.
Добавьте API-ключ PineForge как repository secret с именем PINEFORGE_API_KEY, затем создайте .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)"Храните baseline-отчёт по пути baseline/report.jsonв репозитории, прокомиченным рядом с исходником стратегии. Когда PR намеренно меняет поведение стратегии, автор обновляет baseline в составе того же PR. Этот diff становится постоянной записью того, что и почему изменилось.
Тот же воркфлоу одинаково бежит на GitLab CI, Bitbucket Pipelines или любом runner'е, где есть Docker — меняйте YAML-синтаксис, оставляйте shell-команды как есть.
Exit-коды и проверка parity
Runtime PineForge выходит с кодом 0при чистом прогоне и ненулевым на любой engine-ошибке (некорректный Pine, неподдерживаемый built-in, ошибка парсинга файла данных). CI-системы подхватывают эти exit-коды автоматически — никакой специальной обработки для движковых ошибок не нужно.
Проверка parity — отдельная задача, реализуется в вашем воркфлоу. JSON-схема отчёта стабильна между 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"
}
]
}Для простой числовой проверки сравните summary.net_pnl и summary.total_trades с baseline. Для строгого сравнения сделка-в-сделку диффите весь массив trades — индексы баров входа и выхода, направления и цены fill'а должны совпадать. Когда не совпадают — diff показывает ровно, какая сделка разошлась, на каком баре и какой была разница в цене fill'а.
Практический порог для числовых полей: принимаете дельты с плавающей точкой до 0.01(копейка или базисный пункт на нормированной серии), валите всё, что больше. Для diff'ов сделка-в-сделку любое расхождение в entry_bar или exit_bar — всегда жёсткий fail, независимо от близости PnL: сделка на другом баре означает, что у вас изменилась логика сигнала.
Команды, внедряющие parity CI, обычно гоняют апдейт baseline отдельным шагом, гейченным по PR-метке. Так намеренное улучшение стратегии задокументировано в таймлайне PR — diff baseline всплывает в код-ревью рядом с тем Pine-изменением, что его вызвало.