Особенности торговли с пользовательскими символами

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

В качестве простейшего решения данной задачи можно размещать советник на чарте исходного символа, но получать сигналы (например, с индикаторов) с кастом-символа.

Другой напрашивающийся подход заключается в подмене имен символов при выполнении торговых операций.

Чтобы проверить оба подхода нам потребуется кастом-символ и торговый эксперт.

В качестве интересного практичного примера кастом-символов возьмем эквиобъемные графики нескольких разновидностей.

Эквиобъемный (равнообъемный) график — это график из баров, построенных по принципу равенства заключенного в них объема. На обычном графике каждый новый бар формируется с заданной периодичностью, совпадающей с размером таймфрейма. На эквиобъемном графике каждый бар считается сформированным, когда сумма тиков или реальных объемов достигает предустановленного значения. В этот момент программа начинает подсчет суммы для следующего бара. Разумеется, в процессе подсчета объемов производится контроль движений цены, и мы получаем на графике привычные четверки цен: Open, High, Low, Close.

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

Таким образом, в эксперте EqualVolumeBars.mq5 мы поддержим три режима, то есть, фактически, три типа графика:

  • EqualTickVolumes — эквиобъемные бары по тикам;
  • EqualRealVolumes — эквиобъемные бары по реальным объемам (если они транслируются);
  • RangeBars — равнодиапазонные бары.

Они выбираются с помощью входного параметра WorkMode.

Размер бара и глубина истории для расчета указываются в параметрах TicksInBar и StartDate.

input int TicksInBar = 1000;
input datetime StartDate = 0;

В зависимости от режима, кастом-символ получит суффикс "_Eqv", "_Qrv" или "_Rng", соответственно, с добавлением размера бара.

Хотя горизонтальная ось на эквиобъемном/равнодиапазонном графике по-прежнему обозначает хронологию, временные отметки каждого бара носят произвольный характер и зависят от волатильности (количества или размера сделок) в каждый промежуток времени. В связи с этим таймфрейм графика кастом-символа следует выбирать равным минимальному M1.

Ограничением платформы является то, что все бары имеют равную номинальную длительность, но в случае наших "искусственных" графиков следует помнить, что настоящая длительность у каждого бара своя и может существенно превышать 1 минуту или наоборот быть меньше. Так при достаточно небольшом заданном объеме для одного бара может сложиться ситуация, что новые бары формируются гораздо чаще, чем раз в минуту, и тогда виртуальное время баров кастом-символа будет убегать вперед от реального времени, в будущее. Чтобы такого не происходило, следует увеличить объем бара (параметр TicksInBar) или сдвигать старые бары влево.

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

Считывать историю реальных тиков мы будем с помощью встроенных функций CopyTicks/CopyTicksRange: первая — для подкачки истории пакетами по 10000 тиков, вторая — для запроса новых тиков с момента предыдущей обработки. Весь этот функционал упакован в класс TicksBuffer (полный исходный код прилагается).

class TicksBuffer
{
private:
   MqlTick array[]; // внутренний массив тиков
   int tick;        // инкрементируемый индекс очередного тика для чтения
public:
   bool fill(ulong &cursorconst bool history = false);
   bool read(MqlTick &t);
};

Публичный метод fill предназначен для заполнения внутреннего массива очередной порцией тиков, начиная со времени cursor (в миллисекундах). При этом время в cursor при каждом вызове сдвигается вперед на основе времени последнего прочитанного в буфер тика (обратите внимание, что параметр передается по ссылке).

Параметр history определяет, будет ли использоваться CopyTicks или CopyTicksRange. Как правило, в онлайне мы будем считывать один или несколько новых тиков из обработчика OnTick.

Метод read возвращает один тик из внутреннего массива и сдвигает внутренний указатель (tick) на следующий тик. Если при чтении достигнут конец массива, метод вернет false, что означает, что пора вызвать метод fill.

С помощью данных методов алгоритм обхода истории тиков реализуется следующим образом (данный код опосредованно вызывается из OnInit через таймер).

   ulong cursor = StartDate * 1000;
   TicksBuffer tb;
    
   while(tb.fill(cursortrue) && !IsStopped())
   {
      MqlTick t;
      while(tb.read(t))
      {
         HandleTick(ttrue);
      }
   }

В задействованной здесь функции HandleTick требуется учесть свойства тика t в неких глобальных переменных, в которых контролируется количество тиков, суммарный торговый объем (реальный, если есть), а также дистанция движения цены. В зависимости от режима работы, эти переменные должны по-разному анализироваться на условие формирования нового бара. Так если в эквиобъемном режиме количество тиков превысило TicksInBar, мы должны начать новый бар, сбросив счетчик в 1. При этом время нового бара берется как округленное до минуты время тика.

В этой группе глобальных переменных предусмотрено хранение виртуального времени последнего ("текущего") бара на кастом-символе (now_time), его цен OHLC и объемов.

datetime now_time;
double now_closenow_opennow_lownow_high;
long now_volumenow_real;

Переменные постоянно обновляются как в процессе чтения истории, так и впоследствии, когда эксперт начинает обрабатывать онлайн-тики в реальном времени (к этому мы вернемся чуть позже).

В несколько упрощенном виде алгоритм внутри HandleTick выглядит так:

void HandleTick(const MqlTick &tconst bool history = false)
{
   now_volume++;               // подсчет количества тиков
   now_real += (long)t.volume// суммирование реальных объемов
   
   if(!IsNewBar()) // продолжаем текущий бар
   {
      if(t.bid < now_lownow_low = t.bid;   // отслеживаем колебания цен вниз
      if(t.bid > now_highnow_high = t.bid// и вверх
      now_close = t.bid;                     // обновляем цену закрытия
    
      if(!history)
      {
         // обновляем текущий бар, если находимся не в истории
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            now_volume - !historynow_real);
      }
   }
   else // новый бар
   {
      do
      {
         // сохраняем закрытый бар со всеми атрибутами
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            WorkMode == EqualTickVolumes ? TicksInBar : now_volume,
            WorkMode == EqualRealVolumes ? TicksInBar : now_real);
   
         // округляем время до минуты для нового бара
         datetime time = t.time / 60 * 60;
   
         // предотвращаем бары со старым или одинаковым временем
         // если ушли в "будущее", должны просто взять следующий отсчет M1
         if(time <= now_timetime = now_time + 60;
   
         // начинаем новый бар с текущей цены   
         now_time = time;
         now_open = t.bid;
         now_low = t.bid;
         now_high = t.bid;
         now_close = t.bid;
         now_volume = 1;             // первый тик в новом баре
         if(WorkMode == EqualRealVolumesnow_real -= TicksInBar;
         now_real += (long)t.volume// начальный реальный объем в новом баре
   
         // сохраняем новый бар 0
         WriteToChart(now_timenow_opennow_lownow_highnow_close,
            now_volume - !historynow_real);
      }
      while(IsNewBar() && WorkMode == EqualRealVolumes);
   }
}

Параметр history определяет, происходит ли расчет на истории или уже в реальном времени (на приходящих онлайн-тиках). На истории достаточно формировать каждый бар единожды, в то время как в онлайне текущий бар обновляется с каждым тиком. Это позволяет ускорить обработку истории.

Вспомогательная функция IsNewBar возвращает true, когда выполняется условие для закрытия очередного бара согласно режиму.

bool IsNewBar()
{
   if(WorkMode == EqualTickVolumes)
   {
      if(now_volume > TicksInBarreturn true;
   }
   else if(WorkMode == EqualRealVolumes)
   {
      if(now_real > TicksInBarreturn true;
   }
   else if(WorkMode == RangeBars)
   {
      if((now_high - now_low) / _Point > TicksInBarreturn true;
   }
   
   return false;
}

Функция WriteToChart создает бар с заданными характеристиками, вызывая CustomRatesUpdate.

void WriteToChart(datetime tdouble odouble ldouble hdouble clong vlong m = 0)
{
   MqlRates r[1];
   
   r[0].time = t;
   r[0].open = o;
   r[0].low = l;
   r[0].high = h;
   r[0].close = c;
   r[0].tick_volume = v;
   r[0].spread = 0;
   r[0].real_volume = m;
   
   if(CustomRatesUpdate(SymbolNamer) < 1)
   {
      Print("CustomRatesUpdate failed: "_LastError);
   }
}

Вышеприведенный цикл чтения и обработки тиков выполняется при первичном обращении к истории, после создания или при полном пересчете уже существующего пользовательского символа. Когда же дело доходит до новых тиков, в функции OnTick используется аналогичный код, но уже без флагов "историчности".

void OnTick()
{
   static ulong cursor = 0;
   MqlTick t;
   
   if(cursor == 0)
   {
      if(SymbolInfoTick(_Symbolt))
      {
         HandleTick(t);
         cursor = t.time_msc + 1;
      }
   }
   else
   {
      TicksBuffer tb;
      while(tb.fill(cursor))
      {
         while(tb.read(t))
         {
            HandleTick(t);
         }
      }
   }
   
   RefreshWindow(now_time);
}

Функция RefreshWindow пробрасывает тик в Обзор рынка для пользовательского символа.

Обратите внимание, что проброс тика увеличивает счетчик тиков в баре на 1, в связи с чем при записи счетчика тиков в 0-й бар мы ранее вычитали единицу (см. выражение now_volume - !history при вызове WriteToChart).

Генерация тиков важна, поскольку вызывает на графиках пользовательских инструментов событие OnTick, что потенциально позволяет экспертам, размещенным на таких чартах, торговать. Однако данная технология требует кое-каких дополнительных ухищрений, которые мы рассмотрим позднее.

void RefreshWindow(const datetime t)
{
   MqlTick ta[1];
   SymbolInfoTick(_Symbolta[0]);
   ta[0].time = t;
   ta[0].time_msc = t * 1000;
   if(CustomTicksAdd(SymbolNameta) == -1)
   {
      Print("CustomTicksAdd failed:"_LastError" ", (longta[0].time);
      ArrayPrint(ta);
   }
}

Подчеркнем, что время генерируемого кастом-тика всегда ставится равным метке текущего бара, поскольку мы не можем оставить время реального тика: если оно ушло вперед более, чем на 1 минуту, и мы пошлем такой тик в Обзор рынка, терминал создаст следующий бар M1, что нарушит нашу "эквиобъемную" структуру, ведь у нас бары формируются не по времени, а по наполнению объемом (и мы сами контролируем данный процесс).

В принципе, мы могли бы добавлять к каждому тику одну миллисекунду, но у нас нет гарантии, что в баре не потребуется сохранить более 60000 тиков (например, если пользователь закажет график с некоторым размахом цены, который непредсказуем с точки зрения того, сколько тиков потребуется для такого движения).

В режимах по объемам теоретически возможна интерполяция секундной и миллисекундной составляющей времени тика по линейным формулам:

  • EqualTickVolumes — (now_volume - 1) * 60000 / TicksInBar;
  • EqualRealVolumes — (now_real - 1) * 60000 / TicksInBar;

Однако это — не более чем средство идентификации тиков, а не попытка приближения времени "искусственных" тиков к времени реальных. И речь здесь не только, и не столько об утрате неравномерности реального потока тиков, которая сама по себе уже будет приводить к отличиям в цене между оригинальным символом и генерируемом на его основе кастом-символом.

Основная проблема заключается в необходимости округления времени тиков по границе бара M1 и их "упаковки" в пределах одной минуты (см. далее врезку про специальные виды графиков). Например, очередной тик с реальным временем 12:37:05'123 становится 1001-м по счету тиком и должен сформировать новый эквиобъемный бар. Однако бар M1 может быть помечен временем только с точностью до минуты, то есть 12:37. В результате, реальная цена инструмента на 12:37 не будет совпадать с ценой в тике, который дал цену Open для эквиобъемного бара 12:37. Кроме того, если следующие 1000 тиков растянутся на несколько минут, мы все равно будем вынуждены "сжимать" их время, чтобы не достичь метки 12:38.

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

Важно отметить, что проброс тиков делается в данной версии генератора только онлайн, в то время как на истории кастом-тики не генерируются! Это сделано в целях ускорения создания котировок. Если вам требуется формировать историю тиков, невзирая на более медленный процесс, эксперт EqualVolumeBars.mq5 следует адаптировать: избавиться от функции WriteToChart, и всю генерацию выполнять с помощью CustomTicksReplace/CustomTicksAdd. При этом следует помнить, что оригинальное время тиков должно подменяться на другое — в пределах минутного бара, чтобы не нарушить структуру формируемого эквиобъемного графика.

Посмотрим, как работает EqualVolumeBars.mq5. Вот рабочий график EURUSD M15, на котором размещен эксперт, и созданный им эквиобъемный график, где на каждый бар отведена 1000 тиков.

Эквиобъемный график EURUSD с 1000 тиков на бар, сгенерированный экспертом EqualVolumeBars

Эквиобъемный график EURUSD с 1000 тиков на бар, сгенерированный экспертом EqualVolumeBars

Заметьте, что тиковые объемы на всех барах равны, за исключением последнего, который еще только формируется (подсчет тиков продолжается).

В журнал выводится статистика.

Creating "EURUSD.c_Eqv1000"
Processing tick history...
End of CopyTicks at 2022.06.15 12:47:51
Bar 0: 2022.06.15 12:40:00 866 0
2119 bars written in 10 sec
Open "EURUSD.c_Eqv1000" chart to view results

Проверим другой режим работы — равнодиапазонный. Ниже представлен график, на котором размах каждого бара составляет 250 пунктов.

Равнодиапазонный график EURUSD с барами размахом 250 пунктов, сгенерированный экспертом EqualVolumeBars

Равнодиапазонный график EURUSD с барами размахом 250 пунктов, сгенерированный EqualVolumeBars

Для биржевых инструментов эксперт позволяет использовать режим реальных объемов, например, так:

Исходный и эквиобъемный график Ethereum с реальным объемом 10000 на бар, сгенерированный экспертом EqualVolumeBars

Исходный и эквиобъемный график Ethereum с реальным объемом 10000 на бар

Таймфрейм рабочего символа при размещении эксперта-генератора не важен, поскольку для расчетов всегда используется тиковая история.

Вместе с тем, таймфрейм графика пользовательского символа должен быть равен M1 (как наименьший доступный в терминале). Таким образом, время баров, как правило, максимально близко (насколько возможно) соответствует моментам их формирования. Однако при сильных движениях на рынке, когда количество тиков или размер объемов формирует несколько баров в минуту, время баров будет убегать вперед от реального. Когда рынок успокоится, ситуация с временными отметками эквиобъемных баров нормализуется. На потоке онлайн-цен это не сказывается, так что, вероятно, не особенно критично, поскольку весь смысл использования равнообъемных или равнодиапазонных баров заключается в отвязке от абсолютного времени.

К сожалению, имя исходного символа и созданного на его основе пользовательского никак нельзя связать средствами самой платформы. Было бы удобно иметь среди свойств пользовательского символа строковое поле "origin" (источник), в которое мы могли бы записать имя реального рабочего инструмента. По умолчанию оно было бы пустым, но если его заполнить, то платформа могла бы автоматически и прозрачно для пользователя подменять символ во всех торговых приказах и запросах истории. В принципе, среди свойств пользовательских символов есть подходящее по смыслу поле SYMBOL_BASIS, но поскольку мы не можем гарантировать, что произвольные генераторы пользовательских символов (любые MQL-программы), будут корректно заполнять его или использовать именно по такому назначению, закладываться на его использование нельзя.

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

Для решения задачи разработан класс CustomOrder (см. прилагаемый файл CustomOrder.mqh). Он содержит методы-обертки для всех функций MQL API, связанных с отправкой торговых приказов и запросом истории, в которых имеется строковый параметр с именем инструмента. В этих методах производится подмена пользовательского символа на текущий рабочий или обратно. Прочие функции API не требуют "перехвата". Ниже представлен фрагмент.

class CustomOrder
{
private:
   static string workSymbol;
   
   static void replaceRequest(MqlTradeRequest &request)
   {
      if(request.symbol == _Symbol && workSymbol != NULL)
      {
         request.symbol = workSymbol;
         if(MQLInfoInteger(MQL_TESTER)
            && (request.type == ORDER_TYPE_BUY
            || request.type == ORDER_TYPE_SELL))
         {
            if(TU::Equal(request.priceSymbolInfoDouble(_SymbolSYMBOL_ASK)))
               request.price = SymbolInfoDouble(workSymbolSYMBOL_ASK);
            if(TU::Equal(request.priceSymbolInfoDouble(_SymbolSYMBOL_BID)))
               request.price = SymbolInfoDouble(workSymbolSYMBOL_BID);
         }
      }
   }
   
public:
   static void setReplacementSymbol(const string replacementSymbol)
   {
      workSymbol = replacementSymbol;
   }
   
   static bool OrderSend(MqlTradeRequest &requestMqlTradeResult &result)
   {
      replaceRequest(request);
      return ::OrderSend(requestresult);
   }
   ...

Обратите внимание, что основной рабочий метод replaceRequest делает не только подмену символа, но и текущих цен Ask и Bid. Это связано с тем, что многие кастом-инструменты, такие как наш эквиобъемный график, имеют виртуальное время, отличное от времени настоящего символа-прототипа. Поэтому эмулируемые тестером цены кастом-инструмента рассинхронизированы по времени с соответствующими ценами реального инструмента.

Данный артефакт происходит только в тестере. При торговле онлайн график кастом-символа будет обновляться (по ценам) синхронно с реальным, хотя метки баров будут отличаться (один "искусственный" бар M1 имеет реальную длительность больше или меньше минуты, и момент его отсчета не кратен минуте). Таким образом, данное преобразование цены является, скорее, предосторожностью, чтобы не получать реквоты в тестере. Однако в тестере нам обычно и не нужно делать подмену символа, так как тестер умеет торговать кастом-символом (в отличие от сервера брокера). Далее мы просто ради интереса сравним результаты тестов, запущенных как с подменой, так и без подмены символа.

Для минимизации правок клиентского исходного кода предусмотрены глобальные функции и макросы следующего вида (для всех методов CustomOrder):

  bool CustomOrderSend(const MqlTradeRequest &requestMqlTradeResult &result)
  {
    return CustomOrder::OrderSend((MqlTradeRequest)requestresult);
  }
  
  #define OrderSend CustomOrderSend

Они позволяют автоматически перенаправлять все вызовы стандартных функций API на методы класса CustomOrder — для этого достаточно включить CustomOrder.mqh в эксперт и задать рабочий символ, например, в параметре WorkSymbol:

  #include <CustomOrder.mqh>
  #include <Expert/Expert.mqh>
  ...
  input string WorkSymbol = "";
  
  int OnInit()
  {
    if(WorkSymbol != "")
    {
      CustomOrder::setReplacementSymbol(WorkSymbol);
      
      // инициируем открытие вкладки графика рабочего символа (в визуальном режиме тестера)
      MqlRates rates[1];
      CopyRates(WorkSymbolPERIOD_CURRENT01rates);
    }
    ...
  }

Важно, чтобы директива #include <CustomOrder.mqh> шла самой первой, перед другими. Таким образом она оказывает эффект на все исходные коды, в том числе и на подключаемые стандартные библиотеки из поставки MetaTrader 5. Если подстановочный символ не задан, подключенный CustomOrder.mqh не оказывает никакого эффекта на эксперт и "прозрачно" передает управление стандартным функциям API.

Теперь у нас все готово для проверки идеи торговли на кастом-символе, включая и сам кастом-символ.

Модифицируем указанным выше способом уже знакомый нам эксперт BandOsMaPro, переименовав в BandOsMaCustom.mq5. Давайте протестируем его на эквиобъемном графике EURUSD с размером бара 1000 тиков, полученном с помощью EqualVolumeBars.mq5.

Режим оптимизации или тестирования — по ценам OHLC M1 (более точные методы не имеют смысла, потому что мы не генерировали тики, а также потому что данная версия эксперта торгует по ценам сформированных баров). Диапазон дат — 2021 год и первая половина 2022 года. Файл с настройками прилагается — BandOsMACustom.set.

В настройках тестера следует не забыть выбрать кастом-символ EURUSD_Eqv1000 и таймфрейм M1, поскольку именно на нем эмулируются эквиобъемные бары.

Когда параметр WorkSymbol пуст, эксперт торгует кастом-символом. Вот его результаты:

Отчет тестера при торговле на эквиобъемного графике EURUSD_Eqv1000

Отчет тестера при торговле на эквиобъемного графике EURUSD_Eqv1000

Если параметр WorkSymbol равен EURUSD, эксперт торгует парой EURUSD, несмотря на то, что работает на графике EURUSD_Eqv1000. Результаты отличаются, но не сильно.

Отчет тестера при торговле EURUSD с эквиобъемного графика EURUSD_Eqv1000

Отчет тестера при торговле EURUSD с эквиобъемного графика EURUSD_Eqv1000

Но, как уже было сказано в начале раздела, для экспертов, которые торгуют по сигналам индикаторов, существует более простой способ поддержать кастом-символы. Для этого достаточно создавать индикаторы на кастом-символе, а сам эксперт размещать на графике рабочего символа.

Мы можем легко реализовать такой вариант. Назовем его BandOsMACustomSignal.mq5.

Заголовочный файл CustomOrder.mqh больше не понадобится, а вместо входного параметра WorkSymbol добавим два новых:

input string SignalSymbol = "";
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_M1;

Их потребуется передать в конструктор класса BandOsMaSignal, который заведует индикаторами. Ранее в нем везде использовались _Symbol и _Period.

interface TradingSignal
{
   virtual int signal(void);
   virtual string symbol();
   virtual ENUM_TIMEFRAMES timeframe();
};
   
class BandOsMaSignalpublic TradingSignal
{
   int hOsMAhBandshMA;
   int direction;
   const string _symbol;
   const ENUM_TIMEFRAMES _timeframe;
public:
   BandOsMaSignal(const string sconst ENUM_TIMEFRAMES tf,
      const int fastconst int slowconst int signalconst ENUM_APPLIED_PRICE price,
      const int bandsconst int shiftconst double deviation,
      const int periodconst int xENUM_MA_METHOD method): _symbol(s), _timeframe(tf)
   {
      hOsMA = iOsMA(stffastslowsignalprice);
      hBands = iBands(stfbandsshiftdeviationhOsMA);
      hMA = iMA(stfperiodxmethodhOsMA);
      direction = 0;
   }
   ...
   virtual string symbol() override
   {
      return _symbol;
   }
   
   virtual ENUM_TIMEFRAMES timeframe() override
   {
      return _timeframe;
   }
}

Так как символ и таймфрейм для сигналов теперь могут отличаться от символа и периода графика, мы расширили интерфейс TradingSignal методами для их чтения. Передача актуальных значений в конструктор производится в OnInit.

int OnInit()
{
   ...
   strategy = new SimpleStrategy(
      new BandOsMaSignal(SignalSymbol != "" ? SignalSymbol : _Symbol,
         SignalSymbol != "" ? SignalTimeframe : _Period,
         p.fastp.slowSignalOsMAPriceOsMA,
         BandsMABandsShiftBandsDeviation,
         PeriodMAShiftMAMethodMA),
         MagicStopLossLots);
   return INIT_SUCCEEDED;
}

В классе SimpleStrategy метод trade теперь проверяет наступление нового бара не по текущему графику, а согласно свойствам сигнала.

   virtual bool trade() override
   {
      // ищем сигнал один раз на открытии бара нужного символа и таймфрейма
      if(lastBar == iTime(command[].symbol(), command[].timeframe(), 0)) return false;
      
      int s = command[].signal(); // получаем сигнал
      ...
   }

Для сравнительного эксперимента с теми же настройками эксперт BandOsMACustomSignal.mq5 следует запускать на EURUSD (можно M1 или другой таймфрейм), а в параметре SignalSymbol указать EURUSD_Eqv1000. SignalTimeframe следует оставить равным по умолчанию PERIOD_M1. В результате мы получим похожий отчет.

Отчет тестера при торговле на графике EURUSD по сигналам с эквиобъемного символа EURUSD_Eqv1000

Отчет тестера при торговле на графике EURUSD по сигналам с эквиобъемного символа EURUSD_Eqv1000

Количество баров и тиков здесь другое, потому что тестируемым инструментом выбран EURUSD, а не пользовательский EURUSD_Eqv1000.

Все три результата тестов слегка отличаются. Это происходит из-за "упаковки" котировок в минутные бары и незначительной рассинхронизации движения цен оригинального и пользовательского инструмента. Какой из результатов точнее? Это, скорее всего, зависит от конкретной торговой системы и особенностей её реализации. В случае нашего эксперта BandOsMa с контролем открытия баров наиболее реалистичные показатели должны быть у варианта с прямой торговлей на EURUSD_Eqv1000. Но в принципе, почти всегда выполняется эмпирическое правило, что из нескольких альтернативных проверок наиболее достоверной является наименее прибыльная.

Итак, мы разобрали пару приемов по адаптации экспертов для торговли на пользовательских символах, имеющих прототип среди рабочих символов брокера. Однако такая ситуация не является обязательной. Во многих случаях кастом-символы генерируются на основе данных из внешних систем, таких как криптобиржи. Торговля на них должна выполняться с использованием их публичного API с помощью сетевых функций MQL5.

Эмуляция специальных видов графиков с помощью пользовательских символов
 
Многие трейдеры применяют на практике специальные виды графиков, в которых непрерывное реальное время исключено из рассмотрения. Сюда относятся не только эквиобъемные и равнодиапазонные бары, но также Ренко, Point-And-Figure (PAF), Kagi, и другие. Пользовательские символы позволяют эмулировать эти виды графиков в MetaTrader 5 с помощью графиков таймфрейма M1, но к ним следует относиться с осторожностью, когда речь заходит о тестировании торговых систем, а не техническом анализе.
 
У специальных видов графиков фактическое время открытия бара (с точностью до миллисекунды) практически всегда не совпадает ровно с минутой, которой будет маркирован бар M1. Таким образом, цена открытия кастом-бара отличается от цены открытия бара M1 стандартного символа.
 
Тем более будут отличаться и прочие цены OHLC, потому что реальная длительность формирования бара M1 на специальном графике не равна одной минуте. Например, 1000 тиков для эквиобъемного графика могут накапливаться в течение 5 минут.
 
Цена закрытия кастом-бара также не соответствует реальному времени закрытия, потому что кастом-бар — это, технически, бар М1, т.е. он имеет номинальную длительность 1 минута.
 
Особую осторожность следует проявлять для таких видов графиков как классический Ренко или PAF. Дело в том, что в них разворотные бары имеют цену открытия с гепом от закрытия предыдущего бара. Таким образом, цена открытия становится предиктором будущего ценового движения.
 
Анализ таких графиков предполагается проводить по сформированным барам, то есть их характеристическая цена — цена закрытия, однако тестер при побаровой работе предоставляет для текущего (последнего) бара только цену открытия (режима по ценам закрытия нет). Даже если брать сигналы индикаторов с закрытых баров (обычно, с 1-го), сделки в любом случае совершаются по текущей цене 0-го бара. И даже если обратиться к тиковым режимам, тестер всегда генерирует тики по обычным правилам, руководствуясь опорными точками на основе конфигурации каждого бара. Тестер не учитывает особенностей строения и поведения специальных графиков, которые мы пытаемся визуально эмулировать барами M1.
 
Торговля в тестере по таким символам в любом режиме (по ценам открытия, M1 OHLC или по тикам) сказывается на точности результатов — они слишком оптимистичны и могут служить источником псевдо-граалей. В связи с этим насущно необходимо проверять торговую систему не на отдельном графике Ренко или PAF, а в связке с исполнением приказов на реальном символе.
 
Еще одним полем для применения кастом-символов являются секундные таймфреймы или графики тиков. В этом случае для баров и тиков также генерируется виртуальное время, отвязанное от реального. Поэтому такие графики хорошо подходят для оперативного анализа, но требуют дополнительного внимания при разработке и тестировании торговых стратегий, особенно мультисимвольных.
 
Альтернативой для любых пользовательских символов являет самостоятельный расчет массивов баров и тиков внутри эксперта или индикатора. Однако отладка и визуализация таких структур требует дополнительных усилий.