Перенос стратегии из TradingView в XTester: Pine Script → C#

Перенос стратегии из TradingView в XTester: Pine Script → C#

У стратегии из TradingView зелёная кривая капитала. Код Pine Script загружен в конвертер, новый проект на C# собирается без ошибок, первый бэктест завершён. Но входы сдвинулись на одну свечу, часть стопов сработала иначе, а после нескольких сигналов подряд размер позиции перестал совпадать с оригиналом.

Такое расхождение встречается при переносе торговых стратегий регулярно: можно правильно перевести команды и всё равно получить другую торговую систему.

Формула сигнала — только часть стратегии. Её поведение также задают модель времени, правила создания и исполнения заявок, состояние позиции, размер ордера, комиссии, проскальзывание, источник данных и работа с незакрытой свечой. Пока эти правила не описаны явно, слово «конвертация» означает слишком многое.

В XTester 0.0.88 появился Strategy Transfer — поток переноса сторонней стратегии в отдельный проект XTester. Он принимает исходник, строит модель поведения, показывает несовместимости, генерирует C#, компилирует его и формирует отчёт. Но даже такой конвейер не доказывает, что два бэктеста обязаны совпасть. Последняя часть работы остаётся исследовательской: запустить порт на сопоставимых данных, сравнить сделки и объяснить расхождения.

Ниже — практический порядок этой проверки на примере Pine Script из TradingView. Пример учебный: он не является торговой рекомендацией и не предлагает готовую прибыльную стратегию.

Переносится не код — переносится контракт исполнения

Допустим, исходная идея звучит просто:

Открывать длинную позицию, когда быстрая EMA пересекает медленную, но только при восходящем тренде на четырёхчасовом таймфрейме. Рассчитывать размер каждой заявки как 10% доступного капитала на момент открытия сделки, разрешить не более двух однонаправленных входов в позиции, поставить стоп 3% и цель 6%.

Из этого описания ещё нельзя однозначно восстановить сделки.

Нужно знать хотя бы следующее:

  1. Сигнал считается на закрытии свечи или на каждом обновлении цены?
  2. Рыночная заявка исполняется в момент сигнала, на следующем тике или на открытии следующего бара?
  3. Четырёхчасовой фильтр использует только закрытую HTF-свечу или меняющееся значение текущей?
  4. «10% капитала» — это размер заявки или допустимый убыток по стопу, и от какой базы он считается?
  5. pyramiding = 2 означает максимум два входа в одном направлении; как целевая среда считает первый и повторный вход?
  6. Повторный сигнал увеличивает позицию, игнорируется или переворачивает её?
  7. Стоп и цель выставляются от цены первого входа, средней цены позиции или пересчитываются после добавления?
  8. Какие комиссия, проскальзывание, funding-платежи, сессия и часовой пояс использовались?

Ответы образуют контракт исполнения стратегии. Синтаксис Pine Script или C# — лишь одна его реализация.

Это различие полезно далеко за пределами конвертации. Если трейдер не может записать контракт исполнения своей стратегии, он не может надёжно объяснить ни её бэктест, ни расхождение между тестером и реальным запуском.

Сквозной пример: код короткий, семантика длинная

Упростим исходник до такого Pine Script:

//@version=6
strategy(
    "HTF Trend Port Example",
    overlay = true,
    initial_capital = 10000,
    default_qty_type = strategy.percent_of_equity,
    default_qty_value = 10,
    pyramiding = 2,
    commission_type = strategy.commission.percent,
    commission_value = 0.08,
    slippage = 2
)

fast = ta.ema(close, 20)
slow = ta.ema(close, 50)

if timeframe.in_seconds() >= timeframe.in_seconds("240")
    runtime.error("The chart timeframe must be lower than 240 minutes.")

htfClose = request.security(
    syminfo.tickerid, "240", close[1],
    lookahead = barmerge.lookahead_on
)
htfEma = request.security(
    syminfo.tickerid, "240", ta.ema(close, 50)[1],
    lookahead = barmerge.lookahead_on
)

longSignal = ta.crossover(fast, slow) and htfClose > htfEma

if longSignal
    strategy.entry("L", strategy.long)

strategy.exit(
    "LX", "L",
    stop = strategy.position_avg_price * 0.97,
    limit = strategy.position_avg_price * 1.06
)

В нём около сорока строк. Для переноса придётся разобрать гораздо больше, чем ta.ema, strategy.entry и strategy.exit.

В декларации стратегии спрятаны начальный капитал, способ расчёта размера, pyramiding, комиссия и проскальзывание. В request.security() зашит выбор подтверждённых данных старшего таймфрейма. strategy.position_avg_price означает, что защитные уровни зависят от средней цены позиции и могут измениться после добавления. А отсутствие специальных параметров расчёта и исполнения оставляет в силе стандартную модель брокерского эмулятора TradingView.

Копирование одного условия пересечения EMA создаст новую стратегию с похожим сигналом. Остальной контракт исполнения при этом потеряется.

Семь мест, где сделки обычно расходятся

1. Свеча сигнала и свеча исполнения — не одно и то же

По умолчанию стратегия TradingView создаёт заявку после расчёта, а брокерский эмулятор может исполнить её не раньше следующего доступного тика. При стандартном расчёте на закрытии бара следующим доступным тиком обычно становится открытие следующей свечи.

Поэтому фраза «вход при пересечении EMA» неполна. В исходном отчёте метка сигнала может относиться к закрытию бара, а цена сделки — к следующему открытию.

Если порт откроет позицию по close сигнальной свечи, он получит систематически более ранние входы. Иногда разница почти незаметна. На гэпе, быстром рынке или коротком таймфрейме она меняет и цену, и размер позиции, и дальнейшую траекторию стопа.

В нашем примере есть ещё один эффект, связанный со временем расчёта. strategy.exit() рассчитывает уровни от strategy.position_avg_price, но calc_on_order_fills не включён. При стандартных настройках вход может исполниться на открытии следующего бара, а новый расчёт с уже известной средней ценой позиции произойдёт только на закрытии. Лишь тогда код сможет создать stop- и limit-заявки от этой цены. Если в Strategy Properties включён пересчёт On order fill, стратегия получает среднюю цену и может выставить защитные заявки сразу после исполнения. Значит, эту настройку нужно переносить вместе с кодом.

На исторических барах calc_on_order_fills также может создавать lookahead bias: пересчёт после исполнения способен видеть финальные OHLC текущего бара. Эту настройку нужно сохранять при воспроизведении эталона, но не следует включать её только ради более раннего выставления защиты.

Первый вопрос при расхождении на один бар:

Мы сравниваем момент появления условия или момент фактического исполнения заявки?

2. Исторический бар и текущий бар живут по разным правилам

Pine Script проходит исторические бары последовательно. На текущей realtime-свече расчёт может повторяться по мере поступления обновлений. Перед пересчётом действует rollback: временные значения открытого бара возвращаются к подтверждённому состоянию, если код не использует специальные механизмы внутрибалочной устойчивости.

Из-за этого условие может появиться внутри свечи, исчезнуть до её закрытия и никогда не сохраниться в истории в том виде, в котором его видел трейдер в реальном времени.

TradingView называет перерисовкой (repainting) широкий класс поведения, при котором исторические расчёты и расчёты в реальном времени или графики ведут себя по-разному. Не всякая перерисовка бесполезна или обманчива. Но для торговых заявок разницу необходимо описать.

При переносе нужно зафиксировать:

  • расчёт только по закрытым барам или внутри бара;
  • нужны ли промежуточные тики;
  • какое состояние сохраняется между обновлениями;
  • может ли сигнал исчезнуть до закрытия свечи;
  • доступна ли в целевой среде сопоставимая детализация данных.

Если исходная стратегия зависит от последовательности внутрибалочных тиков, а порт получает только OHLC, буквальное совпадение может быть недостижимо. Результат должен явно зафиксировать ограничение и выбранную пользователем адаптацию.

3. Stop-limit — не «ещё один стоп»

Market, limit, stop и stop-limit описывают разные последовательности событий.

Особенно часто теряется семантика stop-limit. Сначала цена должна активировать stop. Только после этого появляется limit-заявка, которая может исполниться по заданной цене или лучше — либо не исполниться вовсе. Замена такого ордера обычным stop-market меняет вероятность входа и цену. Замена обычным limit меняет момент, с которого заявка существует.

То же относится к trailing, OCO, bracket, amend и callback-логике исполнения. Если в целевом API нет прямого аналога, корректный перенос требует решения:

  • выразить поведение другой поддерживаемой конструкцией;
  • принять конкретное приближение;
  • вынести часть логики в параметры окружения;
  • исключить блок;
  • остановить перенос.

Молчаливая замена опаснее явного отказа: она создаёт код, который выглядит законченным, но торгует иначе.

4. Pyramiding и разворот меняют состояние позиции

В примере pyramiding = 2 разрешает максимум два однонаправленных входа в одной позиции: первый вход и одно добавление. Одного числа всё равно мало. Нужно восстановить, как платформа считает последовательные входы, как усредняет цену, как обрабатывает повторяющийся ID заявки и что делает сигнал противоположного направления.

Текущий учебный сигнал ta.crossover() возникает один раз на пересечении, поэтому сам по себе этот код не демонстрирует добавление внутри уже открытой позиции. Если реальная стратегия создаёт повторный вход, после его исполнения на следующем расчёте меняется strategy.position_avg_price; вместе со средней ценой пересчитываются стоп и цель. Порт, который сохраняет уровни от первого входа, может разойтись только на выходе.

Сравнивайте список закрытых сделок вместе с состоянием после каждого торгового события:

  • первый вход: время, направление, цена, количество;
  • повторный вход: разрешён ли он, как изменились размер и средняя цена;
  • частичный выход: остаток позиции, комиссии, защитные заявки;
  • полный выход: причина, цена, итоговый объём;
  • противоположный сигнал: игнорирование, закрытие или разворот.

5. «10%» может означать разные деньги

В Pine размер заявки может задаваться свойствами стратегии или явным qty в команде ордера. В примере используется strategy.percent_of_equity: каждая заявка рассчитывается как 10% доступного equity на момент открытия сделки. Это размер позиции, а не риск 10% капитала. При стопе 3% плановый убыток до издержек составляет лишь часть нотионала позиции; гэп и проскальзывание могут его увеличить.

Каждый последующий вход рассчитывается заново от доступного капитала в момент его открытия. Если порт подставит фиксированный объём, первые сделки могут совпасть, а затем количество начнёт расходиться. После этого различия распространятся на P&L, капитал и все последующие размеры. Расчёт размера по риску требует отдельного определения qty из допустимого денежного убытка, расстояния до стопа, стоимости пункта и правил округления инструмента.

В паспорт переноса нужно выписать:

  • единицу размера: контракты, монеты, деньги, доля капитала;
  • базу расчёта: initial capital, cash, available balance, current equity;
  • округление и минимальный шаг количества;
  • лимиты биржи или инструмента;
  • поведение при недостатке средств;
  • влияние плеча и типа маржи.

Совпадение процентов без совпадения базы расчёта ничего не гарантирует.

6. Данные старшего таймфрейма могут знать будущее

request.security() позволяет получать данные другого символа или таймфрейма. Параметр lookahead определяет, может ли исторический расчёт увидеть значение из времени, которое ещё не наступило для текущего бара.

В примере используется распространённый паттерн: barmerge.lookahead_on вместе со сдвигом выражения [1], чтобы брать подтверждённое значение предыдущего HTF-бара. Он корректен только тогда, когда "240" действительно выше таймфрейма графика; поэтому пример содержит явную проверку. При переносе сохраните смысл паттерна. Механическое удаление индекса или замена на «последний доступный close» меняют момент доступности данных.

Три вопроса для любого блока с несколькими таймфреймами:

  1. Какое значение видел алгоритм на историческом баре?
  2. Какое значение он видел на незакрытом realtime-баре?
  3. Когда обновление старшего таймфрейма становилось доступно младшему?

Ошибиться здесь особенно легко: сделки могут стать визуально «лучше», потому что порт начал использовать завершённый четырёхчасовой бар раньше, чем это было возможно в исходной стратегии.

7. Одинаковый код на разных данных — не одинаковый тест

Даже при совпадении логики результаты зависят от входных данных и эмулятора исполнения.

Сверьте symbol, timeframe и остальные условия теста:

  • конкретный источник котировок;
  • спот или фьючерсы;
  • торговые сессии и часовой пояс;
  • диапазон истории и прогрев индикаторов;
  • пропуски и правила построения свечей;
  • комиссии, проскальзывание и funding-платежи;
  • точность цены и количества;
  • модель исполнения внутри бара и дополнительные данные;
  • правила ликвидации и маржи, если они участвуют.

TradingView может использовать Bar Magnifier для более детальных предположений об исполнении внутри исторического бара. Другая среда может располагать иной последовательностью младших свечей или тиков. Это не обязательно ошибка одной из платформ — это разные входные данные и модели.

Паспорт поведения: что подготовить до конвертации

До нажатия «Convert» составьте короткий паспорт исходной стратегии:

  • версия источника: файл, commit SHA или неизменяемый архив;
  • платформа: версия Pine и значимые настройки Strategy Properties;
  • данные: биржа, рынок, символ, таймфрейм, HTF/LTF-запросы;
  • сессия: часовой пояс, торговые часы, исключённые периоды;
  • сигналы: условия входа и выхода, расчёт на закрытии или внутри бара;
  • заявки: market, limit, stop, stop-limit, срок жизни, отмена и замена;
  • позиция: long/short, pyramiding, netting/hedging и разворот;
  • размер: единица, база капитала, округление, ограничения;
  • издержки: комиссия, проскальзывание, funding-платежи и стоимость займа;
  • состояние: переменные между барами, прогрев индикаторов, внешние зависимости;
  • эталон: экспорт закрытых сделок на фиксированном диапазоне.

Такой паспорт сокращает пространство догадок. Если после переноса расходится двадцатая сделка, можно проверить конкретное правило вместо сравнения двух кривых капитала на глаз.

Что делает XTester до проверки сделок

Strategy Transfer принимает исходник как данные и не запускает его. XTester выделяет торговые правила, состояние, расчёт размера позиции, типы заявок и зависимости; несовместимости выносит на решение пользователя; затем создаёт отдельный C#-проект, компилирует его и сверяет результат с моделью поведения. Поддерживаемые источники и этапы подробно описаны в обзоре Strategy Transfer в XTester 0.0.88.

Для практического переноса этого недостаточно. Компиляция и семантическая проверка не подтверждают совпадение сделок. Следующий этап — сравнение порта с эталонным экспортом TradingView.

Как сравнить порт с эталоном

После конвертации проект нужно запустить в Симуляторе на сопоставимом диапазоне. Затем можно использовать «Проверить по эталону…» и передать экспорт TradingView Strategy Tester List of trades либо обобщённый CSV:

time,side,price,qty,action
2026-01-12T08:00:00Z,long,42150.5,0.023,entry
2026-01-13T16:00:00Z,long,44679.0,0.023,exit

XTester жадно сопоставляет входы один к одному в хронологическом порядке с допуском ±N баров: от 0 до 5, по умолчанию 1. Затем рассчитываются совпадение входов по времени и направлению, направление среди совпавших по времени входов и совпадение выходов среди сопоставленных входов.

Статусы по доле совпавших входов:

  • ≥95% — высокое совпадение входов;
  • ≥80% — преимущественное;
  • ≥60% — частичное;
  • ниже — расходится с эталоном.

Эти подписи помогают диагностике и не сертифицируют равенство стратегий. Даже 100% входов в пределах выбранного допуска — по умолчанию ±1 бар, доступно 0–5 — не гарантируют одинаковые цены, размеры, выходы, комиссии и P&L.

Начинайте с первого расхождения

Самый продуктивный порядок:

  1. найдите первую несовпавшую сделку;
  2. сравните данные, на которых появился сигнал;
  3. проверьте момент создания и исполнения заявки;
  4. восстановите состояние позиции перед событием;
  5. сравните размер и округление;
  6. проверьте активные stop/limit-заявки;
  7. только после объяснения переходите к следующему расхождению.

Почему первая? Поздние различия часто являются её последствиями. Один пропущенный вход меняет equity, следующий размер позиции, среднюю цену, защитные уровни и весь остаток цепочки.

Полезно вести журнал:

  1. Временные метки расходятся на один бар. Сначала проверьте, что обе метки относятся к исполнению заявки. Строка TradingView List of trades описывает исполненную сделку, а не момент появления сигнала. Если обе метки — время исполнения, исправьте момент исполнения портированной стратегии; допуск в один бар используйте только для поиска кандидатов на сопоставление. Если один набор содержит время сигнала, сначала приведите семантику событий к одному виду.
  2. Второй вход отсутствует. Причина: в порте pyramiding был равен 1. Решение: исправить контракт позиции.
  3. Стоп исполнился по другой цене. Причина: разные данные внутри бара. Решение: зафиксировать отличие модели исполнения.
  4. Qty расходится после пятой сделки. Причина: фиксированный размер вместо процента капитала. Решение: исправить базу расчёта размера.

Так «бэктесты отличаются» превращается в список проверяемых причин.

Когда перенос можно считать рабочим

C#-файл и одна успешная сборка ещё не закрывают перенос.

Практические критерии готовности могут выглядеть так:

  • исходник зафиксирован точной версией;
  • все обязательные файлы и зависимости прочитаны;
  • контракт исполнения заполнен;
  • нет нерешённых существенных несовместимостей;
  • проект компилируется и проходит проверку песочницы;
  • семантическая проверка не нашла потерянных обязательных веток;
  • тесты идут на сопоставимых данных и настройках;
  • первая серия расхождений объяснена;
  • размеры, комиссии и защитные заявки проверены отдельно;
  • отчёт перечисляет все принятые адаптации;
  • трейдер понимает, что именно остаётся неподтверждённым.

Для простого bar-close crossover приемлемое соответствие может быть высоким. Для стратегии с тиками, закрытыми библиотеками, нестандартными ордерами или брокерской margin-моделью честный результат может быть ограниченным. Это не провал процесса. Провал — скрыть ограничение и назвать результат идентичным.

Что Strategy Transfer не обещает

Конвертер не доказывает:

  • что результаты двух платформ будут равны;
  • что сгенерированная стратегия прибыльна;
  • что историческое поведение сохранится в live;
  • что отсутствующая закрытая библиотека восстановлена точно;
  • что внутрибалочная логика воспроизводима без тех же тиков;
  • что высокий процент совпавших входов означает одинаковый риск.

Он решает другую задачу: делает перенос проверяемым. Исходник, модель поведения, принятые решения, сгенерированный проект, отчёт и посделочное сравнение образуют цепочку, которую можно разобрать по шагам.

Для трейдера это важнее обещания «конвертировать в один клик». Один клик экономит минуты. Явная семантика может спасти недели оптимизации не той стратегии.

Короткий рабочий маршрут

Если у вас есть Pine Script из TradingView, порядок такой:

  1. Зафиксируйте исходник и экспорт List of trades на выбранном диапазоне.
  2. Сохраните Strategy Properties: капитал, расчёт размера, pyramiding, комиссии, проскальзывание и параметры расчёта.
  3. Выпишите зависимости от нескольких таймфреймов, внутрибалочных данных и перерисовки.
  4. В XTester откройте Мастер проектов и выберите конвертацию сторонней стратегии.
  5. Проверьте модель поведения и не принимайте несовместимости автоматически.
  6. Создайте отдельное целевое окружение с тем же рынком, символом, timeframe и издержками.
  7. Скомпилируйте порт и прочитайте итоговый отчёт о конвертации.
  8. Запустите Симулятор и сравните закрытые сделки с эталоном.
  9. Разберите первое расхождение до причины.
  10. Сохраните принятые отличия как часть документации стратегии.

Итогом станет C#-проект с зафиксированными настройками, отличиями и понятной границей достоверности.

Если переносите стратегию сейчас, начните с обзора Strategy Transfer, затем зафиксируйте эталонный List of trades до запуска конвертации.

Источники

Материал носит информационный характер и не является инвестиционной рекомендацией. Пример стратегии приведён только для разбора переноса. Результаты бэктеста не гарантируют будущую доходность.

Материал носит информационный характер и не является инвестиционной рекомендацией. Результаты бэктеста не гарантируют будущую доходность.