Пользовательские символы: основы применения на практике

Stanislav Korotky | 30 июля, 2020

В MetaTrader 5 появилась возможность создавать так называемые пользовательские символы с собственными котировками и тиками. Доступ к ним возможен как из интерфейса терминала, так и на программном уровне через MQL API. Пользовательские символы отображаются в стандартных графиках, позволяют применять к ним индикаторы, выполнять разметку объектами, и даже строить на их основе торговые стратегии.

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

Дополнительно мы разработаем механизм для адаптации советников к торговле реальным символом, связанным с производным пользовательским символом, на графике которого и размещен эксперт.

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

Эквиобъемные и равнодиапазонные графики (equal volume / range charts)

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

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

К сожалению, ни MetaTrader 4, ни MetaTrader 5 не предоставляют эквиобъемные графики, так сказать, "из коробки". Их нужно специальным образом генерировать.

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

Для MetaTrader 5 мы реализуем тот же алгоритм с применением пользовательских символов. Чтобы упростить себе задачу, возьмем неторгующий эксперт из указанной статьи и адаптируем его под MQL API MetaTrader 5.

Оригинальный файл EqualVolumeBars.mq4 был переименован в EqualVolumeBars.mq5 и слегка модифицирован. В частности, ключевые слова extern, описывающие входные параметры, были заменены на input. Вместо двух параметров StartYear и StartMonth стал применяться один — StartDate. Параметр CustomPeriod, задававший нестандартный таймфрейм в MetaTrader 4, здесь не потребуется и потому был удален.

Отметим, что в MetaTrader 4 объемы — только тиковые, то есть фактически они представляют собой количество тиков (изменений цены) в баре. Оригинальная идея заключалась в обработке баров M1 (с их тиковыми объемами) или внешнего csv-файла с тиками от другого брокера, подсчете поступающих во времени тиков и формировании нового эквиобъемного бара, как только набирается заданное количество тиков. Бары записывались в hst-файл, который можно открыть в MetaTrader 4 как оффлайн-чарт.

Для MetaTrader 5 весь код, связанный с чтением csv-файла и записью hst-файла, нам не потребуется. Вместо него мы можем читать историю реальных тиков и формировать бары с помощью API пользовательских символов. Кроме того, MetaTrader 5 предоставляет возможность трансляции брокером реальных объемов в тиках (для инструментов фондовой и прочих бирж, но, как правило, не Forex). Если этот режим включен, то мы можем строить эквиобъемные бары не по количеству тиков, а действительно по реальным объемам.

Входной параметр FromM1 определяет, будет ли эксперт обрабатывать бары M1 (true, по умолчанию) или историю тиков (false). При обработке тиков не стоит сразу выбирать слишком отдаленное прошлое в качестве начала, так как это может потребовать заметного времени и места на диске. Если Вы уже экспериментировали с тиковой историей, то представляете возможности своего компьютера и, вероятно, оценили потенциал располагаемых ресурсов.

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

Тип графика — EqualTickVolumes, EqualRealVolumes, RangeBars — задается с помощью входного параметра WorkMode.

С пользовательскими символами проще всего работать с помощью библиотеки Symbol (автор fxsaber). В эксперт она подключается директивой include:

  #include <Symbol.mqh>

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

  if(!SymbolSelect(symbolName, true))
  {
    const SYMBOL Symb(symbolName);
    Symb.CloneProperties(_Symbol);
    
    if(!SymbolSelect(symbolName, true))
    {
      Alert("Can't select symbol:", symbolName, " err:", GetLastError());
      return INIT_FAILED;
    }
  }

где symbolName — строка с именем пользовательского символа.

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

Когда появляется новый эквиобъемный бар или изменяется текущий, вызывается функция WriteToFile, которая в случае MetaTrader 5 реализована с помощью вызова CustomRatesUpdate:

  void WriteToFile(datetime t, double o, double l, double h, double c, long v, long 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;
    
    int code = CustomRatesUpdate(symbolName, r);
    if(code < 1)
    {
      Print("CustomRatesUpdate failed: ", GetLastError());
    }
  }

На удивление цикл по барам M1 (режим FromM1 = true) практически не меняется по сравнению с версией MQL4, то есть адаптации функции WriteToFile достаточно для получения работоспособного кода MQL5 по барам M1. Единственный нюанс, который требуется изменить — генерация тиков в RefreshWindow. В MetaTrader 4 это делалось путем отправки сообщений Windows для эмуляции тиков на оффлайн-чарте. В MetaTrader 5 для этих целей используется функция CustomTicksAdd:

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

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

Режим формирования эквиобъемных баров из тиковой истории (FromM1 = false) — несколько сложнее. Здесь требуется считывать историю реальных тиков с помощью встроенных функций CopyTicks/CopyTicksRange. Весь этот функционал упакован в класс TicksBuffer.

  #define TICKS_ARRAY 10000
  
  class TicksBuffer
  {
    private:
      MqlTick array[];
      int tick;
    
    public:
      bool fill(ulong &cursor, const bool history = false)
      {
        int size = history ? CopyTicks(_Symbol, array, COPY_TICKS_ALL, cursor, TICKS_ARRAY) : CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, cursor);
        if(size == -1)
        {
          Print("CopyTicks failed: ", GetLastError());
          return false;
        }
        else if(size == 0)
        {
          if(history) Print("End of CopyTicks at ", (datetime)(cursor / 1000));
          return false;
        }
        
        cursor = array[size - 1].time_msc + 1;
        tick = 0;
      
        return true;
      }
      
      bool read(MqlTick &t)
      {
        if(tick < ArraySize(array))
        {
          t = array[tick++];
          return true;
        }
        return false;
      }
  };

В методе fill тики запрашиваются фрагментами по TICKS_ARRAY и попадают в массив array, откуда по отдельности считываются методом read. С помощью данных методов алгоритм обхода истории тиков реализуется похоже на обход истории баров M1 (полные исходные коды прилагаются).

    TicksBuffer tb;
    
    while(tb.fill(cursor, true) && !IsStopped())
    {
      MqlTick t;
      while(tb.read(t))
      {
        ...
        // New or first bar
        if(IsNewBar() || now_volume < 1)
        {
          WriteToFile(...);
        }
      }
    }

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

Желающие могут сравнить исходные коды EqualVolumeBars.mq4 и получившегося EqualVolumeBars.mq5.

Посмотрим, как работает новый эксперт. Вот рабочий график EURUSD H1, на котором размещен эксперт:

Эксперт EqualVolumeBars на графике EURUSD H1 в MetaTrader 5

Эксперт EqualVolumeBars на графике EURUSD H1 в MetaTrader 5

А вот как выглядит созданный им эквиобъемный график, в котором на каждый бар отведено 1000 тиков.

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

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

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

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

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

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

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

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

Эквиобъемный график LKOH с реальным объемом 10000 на бар, сгенерированный экспертом EqualVolumeBars в MetaTrader 5

Исходный (a) и эквиобъемный (b) график LKOH с реальным объемом 10000 на бар, сгенерированный экспертом EqualVolumeBars в MetaTrader 5

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

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

Графики тиков

График тиков в MetaTrader 5 доступен в окне обзора рынка и по каким-то причинам выполнен способом, отличным от обычных графиков. Он отображает ограниченное количество тиков (насколько я знаю, до 2000), он небольшого размера и не может быть развернут на весь экран, а также лишен всех возможностей, которые предоставляют стандартные чарты, вроде наложения индикаторов, объектов и экспертов.

График тиков в окне обзора рынка в MetaTrader 5

График тиков в окне обзора рынка в MetaTrader 5

Даже не будучи скальпером, можно задаться вопросом, почему для тиков не предусмотрены встроенные средства анализа, особенно с учетом того, что в MetaTrader 5 добавлена поддержка истории реальных тиков и сама платформа позиционируется, среди прочего, и как инструмент для высокочастотной торговли (High Frequency Trading, HFT). Некоторые трейдеры считают тики слишком мелкими, не достойными внимания, сущностями и даже шумом, но другие стараются на них заработать. Поэтому в отсутствие встроенных средств имеет смысл самостоятельно отобразить поток тиков в стандартном чарте, с возможностью масштабирования, применения шаблонов и даже экспертов. Эту возможность нам опять же дают пользовательские символы.

Работу можно поручить уже знакомым нам функциям MQL API, таким как CopyTicks и CustomRatesUpdate. С помощью них несложно реализовать неторгующий эксперт, генерирующий пользовательский символ на основе рабочего символа текущего чарта, причем в истории пользовательского символа каждый бар M1 представляет собой отдельный тик. Пример такого исходного кода приложен к статье в файле Ticks2Bars.mq5. Например, если разместить эксперт на графике EURUSD (таймфрейм не важен), он создаст символ EURUSD_ticks.

Входные параметры эксперта:

Основную работу выполняет функция apply:

  bool apply(const datetime cursor, const MqlTick &t, MqlRates &r)
  {
    static MqlTick p;
    
    // eliminate strange things
    if(t.ask == 0 || t.bid == 0 || t.ask < t.bid) return false;
    
    r.high = t.ask;
    r.low = t.bid;
    
    if(t.last != 0)
    {
      if(RenderBars == OHLC)
      {
        if(t.last > p.last)
        {
          r.open = r.low;
          r.close = r.high;
        }
        else
        {
          r.open = r.high;
          r.close = r.low;
        }
      }
      else
      {
        r.open = r.close = (r.high + r.low) / 2;
      }
      
      if(t.last < t.bid) r.low = t.last;
      if(t.last > t.ask) r.high = t.last;
      r.close = t.last;
    }
    else
    {
      if(RenderBars == OHLC)
      {
        if((t.ask + t.bid) / 2 > (p.ask + p.bid) / 2)
        {
          r.open = r.low;
          r.close = r.high;
        }
        else
        {
          r.open = r.high;
          r.close = r.low;
        }
      }
      else
      {
        r.open = r.close = (r.high + r.low) / 2;
      }
    }
    
    r.time = cursor;
    r.spread = (int)((t.ask - t.bid)/_Point);
    r.tick_volume = 1;
    r.real_volume = (long)t.volume;
  
    p = t;
    return true;
  }

В ней для текущего момента времени cursor поля структуры MqlTick переносятся в поля структуры MqlRates, которая потом и записывается в историю.

Вот как выглядит график пользовательского символа с тиками-барами (для сравнения рядом приведен стандартный тиковый график):

Полнофункциональный график тиков EURUSD в MetaTrader 5

Полнофункциональный график тиков EURUSD в MetaTrader 5

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

Важно отметить, что времена баров на графике тиков-баров — фиктивные. Если режим LoopBack включен, последний бар всегда имеет текущее время с точностью до минуты, а предыдущие бары находятся в прошлом с шагом в 1 минуту (минимальный размер таймфрейма в MetaTrader 5). Если режим LoopBack выключен, времена баров постоянно увеличиваются на 1 минуту, начиная с момента запуска эксперта, то есть все бары сверх начального лимита находятся в виртуальном будущем.

Тем не менее, самый правый бар M1 соответствует самому свежему тику и текущей цене "close" (или "last"). Это позволяет торговать на подобных чартах с помощью эксперта как онлайн, так и в тестере. Разумеется, для работы онлайн следует слегка модифицировать эксперт, поскольку, размещенный на графике символа "XY_ticks", он должен на самом деле торговать исходным инструментом "XY" (пользовательские символы существуют только в терминале и не известны на сервере). В вышеприведенном примере "EURUSD_ticks" следует заменять на "EURUSD" во всех торговых приказах.

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

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

Представленный подход с "тиковыми котировками" позволяет тестировать скальперские стратегии без специального аккумулирования данных во внутренних буферах и расчетов сигналов на их основе, оставляя кодирование в привычных парадигмах индикаторов или объектов.

Временной сдвиг и метаморфозы свечных фигур

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

Свечные фигуры описывают предопределенную геометрию последовательности баров. И все бары формируются по ходу изменения цены во времени. Однако время по своей сути — непрерывно, хотя на чартах искусственно поделено на отрезки, соответствующие барам, и выровнены по некоторой временной зоне (выбранной брокером). Например, если график с часовыми барами (H1) сдвинуть на несколько минут (скажем, на четверть часа), то он, скорее всего, полностью изменит геометрию баров. В результате, существовавшие до того свечные фигуры могут полностью исчезнуть, а новые — появиться в других местах. И это все при том, что price action никак не менялся.

Если посмотреть на некоторые популярные свечные фигуры, можно легко распознать, что они формируются схожими движениями цены, а вся разница в их внешнем представлении вызвана временем отсчета начала баров, из которых складываются характерные очертания. Например, если сдвинуть ось времени на половину бара, то "молот" ("hammer") может трансформироваться в "просвет в облаках" ("piercing"), а "виселица" ("hangman") — в "темную завесу" ("dark cloud cover"). В зависимости от величины сдвига и локальных изменений цены мы можем в ходе того же преобразования получить также медвежье или бычье "поглощение" ("engulfing"). А если переключиться на более мелкий траймфрейм, то все вышеупомянутые фигуры могут оказаться утренней или вечерней "звездой" ("morning star", "evening star").

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

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

Разворотные движения цены и эквивалентные свечные структуры

Разворотные движения цены и эквивалентные свечные структуры

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

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

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

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

Например, если сдвинуть котировки на 5 минут вперед, то бары на графике M15 будут открываться и закрываться на треть "раньше" исходного графика (на каждой 10-ой, 25-ой, 40-ой, 55-ой минуте часа). Если сдвиг незначительный, то фигуры в исходном и пользовательском графиках будут практически идентичны, однако сигналы (вычисляемые по барам, в частности, индикаторные) с пользовательского графика будут приходить с опережением.

Создание подобного сдвинутого по времени пользовательского символа реализовано в эксперте TimeShift.mq5.

Величина сдвига задается во входном параметре Shift (в секундах). Эксперт работает по тикам, предоставляя возможность при старте рассчитать историю трансформированных котировок, начиная с даты, указанной в параметре Start. Далее тики обрабатываются онлайн, если включен режим генерации событий OnTick, для чего предусмотрен параметр EmulateTicks (по умолчанию — true).

Преобразование времени выполняется довольно простым, аналогичным образом и для тиков на истории, и для онлайн тиков: например, в последнем случае с помощь функции add:

  ulong lastTick;
  
  void add()
  {
    MqlTick array[];
    int size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, lastTick + 1, LONG_MAX);
    if(size > 0)
    {
      lastTick = array[size - 1].time_msc;
      for(int i = 0; i < size; i++)
      {
        array[i].time += Shift;
        array[i].time_msc += Shift * 1000;
      }
      if(CustomTicksAdd(symbolName, array) == -1)
      {
        Print("Tick error: ", GetLastError());
      }
    }
  }
  
  void OnTick(void)
  {
    ...
    if(EmulateTicks)
    {
      add();
    }
  }

Ниже представлены исходный и модифицированный графики EURUSD H1.

График EURUSD H1 с экспертом TimeShift

График EURUSD H1 с экспертом TimeShift

После смещения на полчаса (половина бара) картина изменяется.

Пользовательский график EURUSD H1 со сдвигом на полчаса

Пользовательский график EURUSD H1 со сдвигом на полчаса

Знатоки свечных фигур несомненно заметят много отличий, включая и новые сигналы, отсутствующие на исходном графике. Таким образом, набросив на пользовательский график какой-нибудь привычный свечной индикатор, можно удвоить количество алертов и возможностей для торговли. Мы даже могли бы автоматизировать процесс с помощью экспертов, но для этого нужно каким-то образом "научить" их торговать настоящим символом с графика производного "виртуального" символа. Этой задачей мы займемся в самом конце статьи, а пока рассмотрим последний, но, пожалуй, самый популярный тип пользовательских графиков — ренко.

Ренко

Для реализации графиков ренко используем неторгующий эксперт RenkoTicks.mq5. Он генерирует ренко в виде котировок пользовательского инструмента, обрабатывая для этого реальные тики (доступные в MetaTrader 5 от вашего брокера). Котировки (бары) исходного символа и таймфрейм рабочего графика, на который помещен RenkoTicks, не имеют значения.

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

Все ренко-кирпичи формируются на таймфрейме M1. Это сделано намеренно, так как возможны ситуации, когда ренко бары идут очень быстро один за другим — например, при высокой волатильности — и время между ними должно быть минимальное из возможных. Минута — это минимальное расстояние, которое поддерживает MetaTrader 5. Именно поэтому график ренко всегда должен иметь таймфрейм M1. Переключать график ренко на другой таймфрейм не имеет смысла. Время начала каждого "минутного" бара на нем совпадает со временем начала формирования соответствующего кирпича ренко. Время окончания такого "минутного" бара — фиктивное, вместо него следует смотреть время начала следующего "минутного" бара.

К сожалению, иногда несколько баров ренко должно быть сформировано внутри одной минуты. Поскольку MetaTrader 5 этого не позволяет, эксперт в таком случае генерирует бары как последовательности смежных баров M1, искусственно увеличивая отсчеты поминутно. В результате формальное время ренко-баров может не совпадать с фактическим (опережать его). Например, при размере ренко 100 пунктов движение в 300 пунктов, случившееся в 12:00:00 и занявшее 10 секунд, должно было бы создать ренко бары 12:00:00, 12:00:05, 12:00:10. Вместо этого эксперт сгенерирует бары 12:00, 12:01, 12:02.

Когда такое случается в истории котировок, может возникнуть проблема, что такие перенесенные из прошлого ренко кирпичи наложатся на другие, формируемые из последующих баров исходного графика. Допустим, что в 12:02 случилось очередное движение на 100 пунктов и, соответственно, надо бы сгенерировать ренко-кирпич с временем открытия 12:02, но оно уже занято! Для разрешения подобных конфликтов эксперт имеет специальный режим с принудительным увеличением времени очередного сформировавшегося бокса на 1 минуту, если требуемый отсчет уже занят. Данный режим задается параметром SkipOverflows, который равен по умолчанию false (наложения боксов не происходит, они при необходимости "убегают" в будущее). Если SkipOverflows равно true, боксы с совпадающим временем перезаписывают друг друга, в результате чего ренко будет не совсем корректным.

Особо следует отметить, что подобная ситуация с сильным движением и генерацией нескольких кирпичей "передним числом" возможна и в реальном времени - тогда бары будут фактически формироваться в будущем! Так в нашем примере в 12:00:10 уже будут ренко бары со временем открытия 12:00, 12:01, 12:02! Это следует учитывать при анализе и торговле.

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

Из-за особенностей платформы эксперт генерирует фиктивные тики со временем, равным времени открытия последнего ренко бара. Их единственная цель — запустить обработчик OnTick в торгующем эксперте. Если бы тики транслировались с исходного символа на пользовательский без изменений, это испортило бы саму структуру ренко. Так, продолжая пример с сильным движением, рассмотренный выше, можно попробовать в 12:00:10 отправить тик на ренко чарт с фактическим временем. Однако время этого тика будет соответствовать не последнему (текущему) бару 0, а уже бару номер 2 с временем открытия 12:00. В результате такой тик испортит ренко-кирпич 12:00 (находящийся в истории) или выдаст ошибку. Возможно разрушение ренко и зеркальным образом — "медленными" движениями. Если котировки долго находятся в диапазоне одного кирпича, на ренко он остается с прежним временем открытия, однако новые тики могут иметь время, более чем на минуту превышающее 0-й бар ренко. В результате из таких тиков, если их отправить на график ренко, были бы сформированы фантомные бары (кирпичи) в "будущем".

Обратите внимание, что на истории тики ренко формируются в минималистическом стиле — 1 тик на бокс. В онлайне все тики транслируются на ренко.

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

Основные параметры:

Обработку потока тиков и создание на его основе новых баров ренко обеспечивает класс Renko. Основные его компоненты обозначены в следующем псевдокоде:

  class Renko
  {
    protected:
      bool incrementTime(const datetime time);
      void doWriteStruct(const datetime dtTime, const double dOpen, const double dHigh, const double dLow, const double dClose, const double dVol, const double dRealVol, const int spread);
      
    public:
      datetime checkEnding();
      void continueFrom(const datetime time);
      void doReset();
  
      void onTick(const MqlTick &t);
      void updateChartWindow(const double bid = 0, const double ask = 0);
  };

Защищенные методы incrementTime и doWriteStruct выполняют, соответственно, переход на следующий свободный, наиболее близкий заданному времени отсчет M1 для очередного бокса ренко, а также запись самого бокса с помощью вызова CustomRatesUpdate. Три первых метода в публичной части отвечают за инициализацию алгоритма при старте. Эксперт умеет проверять наличие прежних котировок ренко (это делает метод checkEnding, возвращающий дату и время окончания истории) и в зависимости от того, есть они или нет, — либо продолжает с указанного момента с помощью метода continueFrom (с восстановлением значений внутренних переменных), либо использует doReset для обработки тиков с "пустого" состояния.

Метод onTick, как и следует из названия, вызывается на каждом тике (как в истории, так и онлайн) и при необходимости формирует ренко бар с помощью doWriteStruct (алгоритм скопирован с некоторыми исправлениями из кода известного эксперта RenkoLiveChart.mq4). Если в настройках эксперта указана эмуляция тиков, то дополнительно вызывается updateChartWindow. Полные исходные коды приложены к статье.

За "доставку" тиков в объект Renko отвечает класс TickProvider:

  class TickProvider
  {
    public:
      virtual bool hasNext() = 0;
      virtual void getTick(MqlTick &t) = 0;
  
      bool read(Renko &r)
      {
        while(hasNext() && !IsStopped())
        {
          MqlTick t;
          getTick(t);
          r.onTick(t);
        }
        
        return IsStopped();
      }
  };

Он является абстрактным, так как декларирует общий интерфейс для чтения/получения тиков из двух разных источников: истории тиков базового символа при старте эксперта и очереди событий OnTick при работе онлайн. Метод read представляет собой универсальный цикл по тикам, в котором используются виртуальные методы hasNext() и getTick().

Чтение истории тиков производится в классе HistoryTickProvider уже знакомым образом — используется CopyTicksRange и промежуточный буфер MqlTick array[], в который тики запрашиваются по дням:

  class HistoryTickProvider : public TickProvider
  {
    private:
      datetime start;
      datetime stop;
      ulong length;     // in seconds
      MqlTick array[];
      int size;
      int cursor;
      
      int numberOfDays;
      int daysCount;
      
    protected:
      void fillArray()
      {
        cursor = 0;
        do
        {
          size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, start * 1000, MathMin(start + length, stop) * 1000);
          Comment("Processing: ", DoubleToString(daysCount * 100.0 / (numberOfDays + 1), 0), "% ", TTSM(start));
          if(size == -1)
          {
            Print("CopyTicksRange failed: ", GetLastError());
          }
          else
          {
            if(size > 0 && array[0].time_msc < start * 1000) // prevent older than requested data returned
            {
              start = stop;
              size = 0;
            }
            else
            {
              start = (datetime)MathMin(start + length, stop);
              if(size > 0) daysCount++;
            }
          }
        }
        while(size == 0 && start < stop);
      }
    
    public:
      HistoryTickProvider(const datetime from, const long secs, const datetime to = 0): start(from), stop(to), length(secs), cursor(0), size(0)
      {
        if(stop == 0) stop = TimeCurrent();
        numberOfDays = (int)((stop - start) / DAY_LONG);
        daysCount = 0;
        fillArray();
      }
  
      bool hasNext() override
      {
        return cursor < size;
      }
  
      void getTick(MqlTick &t) override
      {
        if(cursor < size)
        {
          t = array[cursor++];
          if(cursor == size)
          {
            fillArray();
          }
        }
      }
  };

Класс провайдера онлайн-тиков CurrentTickProvider намного проще:

  class CurrentTickProvider : public TickProvider
  {
    private:
      bool ready;
      
    public:
      bool hasNext() override
      {
        ready = !ready;
        return ready;
      }
      
      void getTick(MqlTick &t) override
      {
        SymbolInfoTick(_Symbol, t);
      }
  };

Основная часть обработки тиков в сокращенном виде выглядит так:

  const long DAY_LONG = 60 * 60 * 24;
  bool _FirstRun = true;
  
  Renko renko;
  CurrentTickProvider online;
  
  void OnTick(void)
  {
    if(_FirstRun)
    {
      // find existing renko tail to supersede StartFrom
      const datetime trap = renko.checkEnding();
      if(trap > TimeCurrent())
      {
        Print("Symbol/Timeframe data not ready...");
        return;
      }
      if((trap == 0) || Reset) renko.doReset();
      else renko.continueFrom(trap);
  
      HistoryTickProvider htp((trap == 0 || Reset) ? StartFrom : trap, DAY_LONG, StopAt);
      
      const bool interrupted = htp.read(renko);
      _FirstRun = false;
      
      if(!interrupted)
      {
        Comment("RenkoChart (" + (string)RenkoBoxSize + "pt): open ", _SymbolName, " / ", renko.getBoxCount(), " bars");
      }
      else
      {
        Print("Interrupted. Custom symbol data is inconsistent - please, reset or delete");
      }
    }
    else if(StopAt == 0) // process online if not stopped explicitly
    {
      online.read(renko);
    }
  }

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

Сгенерируем ренко на основе EURUSD с размером бокса 100 пунктов, начиная с 2019 года. Для этого поместим эксперт на график EURUSD H1 и оставим все настройки по умолчанию, кроме StartFrom. Таймфрейм играет роль только в момент возобновления работы эксперта при уже имеющейся истории ренко — в этом случае пересчет ренко будет начат с отступом на время бара, куда попадает предпоследний бокс ренко (это последний завершенный бокс).

Например, для исходного EURUSD H1:

График EURUSD H1 с экспертом RenkoTicks

График EURUSD H1 с экспертом RenkoTicks

получим следующий график:

График ренко EURUSD с размером блока 100 пунктов

График ренко EURUSD с размером блока 100 пунктов

Для наглядности на него добавлена пара индикаторов МА.

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

Рабочий эксперт на пересечении двух скользящих средних

Выберем одну из самых простых торговых стратегий — пересечение двух скользящих средних. Предыдущий скриншот служит иллюстрацией идеи. Когда быстрая (красная) МА пересекает медленную (синюю) МА вверх или вниз, мы открываем, соответственно, позицию на покупку или продажу. Система — переворотная.

Мы могли бы реализовать эксперт с нуля, но MetaTrader 5 предоставляет Мастер MQL, который способен генерировать готовые эксперты на основе библиотеки стандартных классов (поставляется с терминалом). Это очень удобно для трейдеров, незнакомых с программированием. Получающаяся в результате структура кода является общей для большого числа роботов, и поэтому весьма логичным будет именно на ней отрабатывать и проверять впоследствии нашу основную задачу — адаптацию роботов к торговле на пользовательских символах. Эксперты, созданные без стандартной библиотеки, разумеется, тоже будут адаптированы в рамках той же технологии, но поскольку их строение может существенно отличаться, все нюансы отданы на откуп прикладных разработчиков (иными словами, опытные программисты смогут по аналогии с нашим тестовым экспертом адаптировать любые другие).

В стандартной библиотеке, как ни странно, нет сигнала от пересечения двух MA, хотя это, пожалуй, самая расхожая стратегия (если не в реальных "боевых" роботах, то в курсах обучения алготрейдингу). Нам потребуется написать соответствующий модуль сигнала самостоятельно. Назовем его Signal2MACross.mqh. Ниже приведен его код, отвечающий правилам написания сигналов для MQL Мастера.

В начале идет "шапка" — специальный комментарий с описанием сигнала в особом формате, что делает его доступным из MetaEditor:

  // wizard description start
  //+------------------------------------------------------------------+
  //| Description of the class                                         |
  //| Title=Signals of 2 MAs crosses                                   |
  //| Type=SignalAdvanced                                              |
  //| Name=2MA Cross                                                   |
  //| ShortName=2MACross                                               |
  //| Class=Signal2MACross                                             |
  //| Page=signal_2mac                                                 |
  //| Parameter=SlowPeriod,int,11,Slow MA period                       |
  //| Parameter=FastPeriod,int,7,Fast Ma period                        |
  //| Parameter=MAMethod,ENUM_MA_METHOD,MODE_LWMA,Method of averaging  |
  //| Parameter=MAPrice,ENUM_APPLIED_PRICE,PRICE_OPEN,Price type       |
  //| Parameter=Shift,int,0,Shift                                      |
  //+------------------------------------------------------------------+
  // wizard description end

Имя класса (строка Class) должно совпадать с именем настоящего класса в последующем MQL коде. Сигнал имеет 5 параметров, типичных для пары МА: 2 периода (быстрый и медленный), метод усреднения, тип цены и сдвиг.

Класс унаследован, как это полагается, от CExpertSignal и содержит 2 экземпляра индикаторных объектов CiMA, переменные с рабочими параметрами, методы-"сеттеры" параметров (имена методов должны совпадать с именами в "шапке"). Также в классе переопределены виртуальные методы, которые вызываются при инициализации индикаторов, проверке настроек и детекции сигналов на покупку и продажу.

  class Signal2MACross : public CExpertSignal
  {
    protected:
      CiMA              m_maSlow;         // object-indicator
      CiMA              m_maFast;         // object-indicator
      
      // adjustable parameters
      int               m_slow;
      int               m_fast;
      ENUM_MA_METHOD    m_method;
      ENUM_APPLIED_PRICE m_type;
      int               m_shift;
      
      // "weights" of market models (0-100)
      int               m_pattern_0;      // model 0 "fast MA crosses slow MA"
  
    public:
                        Signal2MACross(void);
                       ~Signal2MACross(void);
                       
      // parameters setters
      void              SlowPeriod(int value) { m_slow = value; }
      void              FastPeriod(int value) { m_fast = value; }
      void              MAMethod(ENUM_MA_METHOD value) { m_method = value; }
      void              MAPrice(ENUM_APPLIED_PRICE value) { m_type = value; }
      void              Shift(int value) { m_shift = value; }
      
      // adjusting "weights" of market models
      void              Pattern_0(int value) { m_pattern_0 = value; }
      
      // verification of settings
      virtual bool      ValidationSettings(void);
      
      // creating the indicator and timeseries
      virtual bool      InitIndicators(CIndicators *indicators);
      
      // checking if the market models are formed
      virtual int       LongCondition(void);
      virtual int       ShortCondition(void);
  
    protected:
      // initialization of the indicators
      bool              InitMAs(CIndicators *indicators);
      
      // getting data
      double            FastMA(int ind) { return(m_maFast.Main(ind)); }
      double            SlowMA(int ind) { return(m_maSlow.Main(ind)); }
  };

Класс описывает единственную стратегию (паттерн или модель): когда быстрая MA пересекает медленную, инициируется покупка (при пересечении вверх) или продажа (при пересечении вниз). Вес модели равен по умолчанию 100.

  Signal2MACross::Signal2MACross(void) : m_slow(11), m_fast(7), m_method(MODE_LWMA), m_type(PRICE_OPEN), m_shift(0), m_pattern_0(100)
  {
  }

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

  int Signal2MACross::LongCondition(void)
  {
    const int idx = StartIndex();
    
    if(FastMA(idx) > SlowMA(idx))
    {
      return m_pattern_0;
    }
    return 0;
  }
  
  int Signal2MACross::ShortCondition(void)
  {
    const int idx = StartIndex();
  
    if(FastMA(idx) < SlowMA(idx))
    {
      return m_pattern_0;
    }
    return 0;
  }

Функция StartIndex определена в родительском классе. Как видно из кода, индекс представляет собой ни что иное как номер бара, для которого анализируется наличие сигнала. Если в настройках эксперта указана работа по тикам (Expert_EveryTick = true, см. ниже), то "стартовый" индекс равен 0, а если нет (т.е. работа ведется только по закрытым барам), то индекс равен 1.

Файл Signal2MACross.mqh следует положить в папку MQL5/Include/Expert/Signal/MySignals, после чего перезапустить MetaEditor (если он был открыт), чтобы "подхватить" новый модуль в Мастере MQL.

Теперь мы можем сгенерировать эксперт на основе нашего сигнала. Выберем пункт меню File | New и тем самым откроем диалог Мастера, а далее по шагам:

  1. выберем пункт Expert Adviser (generate),
  2. зададим имя эксперта, например, Experts\Examples\MA2Cross,
  3. добавим наш сигнал "Signals of 2 MAs crosses",
  4. оставим опцию без трейлинга "Trailing stop not used",
  5. оставим опцию управления капиталом "Trading with fixed volume".

В результате получим исходный код эксперта вроде приведенного ниже:

  #include <Expert\Expert.mqh>
  #include <Expert\Signal\MySignals\Signal2MACross.mqh>
  #include <Expert\Trailing\TrailingNone.mqh>
  #include <Expert\Money\MoneyFixedLot.mqh>
  
  //+------------------------------------------------------------------+
  //| Inputs                                                           |
  //+------------------------------------------------------------------+
  // inputs for expert
  input string             Expert_Title              = "MA2Cross";  // Document name
  ulong                    Expert_MagicNumber        = 7623;
  bool                     Expert_EveryTick          = false;
  // inputs for main signal
  input int                Signal_ThresholdOpen      = 10;          // Signal threshold value to open [0...100]
  input int                Signal_ThresholdClose     = 10;          // Signal threshold value to close [0...100]
  input double             Signal_PriceLevel         = 0.0;         // Price level to execute a deal
  input double             Signal_StopLevel          = 0.0;         // Stop Loss level (in points)
  input double             Signal_TakeLevel          = 0.0;         // Take Profit level (in points)
  input int                Signal_Expiration         = 0;           // Expiration of pending orders (in bars)
  input int                Signal_2MACross_SlowPeriod = 11;         // 2MA Cross(11,7,MODE_LWMA,...) Slow MA period
  input int                Signal_2MACross_FastPeriod = 7;          // 2MA Cross(11,7,MODE_LWMA,...) Fast Ma period
  input ENUM_MA_METHOD     Signal_2MACross_MAMethod  = MODE_LWMA;   // 2MA Cross(11,7,MODE_LWMA,...) Method of averaging
  input ENUM_APPLIED_PRICE Signal_2MACross_MAPrice   = PRICE_OPEN;  // 2MA Cross(11,7,MODE_LWMA,...) Price type
  input int                Signal_2MACross_Shift     = 0;           // 2MA Cross(11,7,MODE_LWMA,...) Shift
  input double             Signal_2MACross_Weight    = 1.0;         // 2MA Cross(11,7,MODE_LWMA,...) Weight [0...1.0]
  // inputs for money
  input double             Money_FixLot_Percent      = 10.0;        // Percent
  input double             Money_FixLot_Lots         = 0.1;         // Fixed volume
  
  //+------------------------------------------------------------------+
  //| Global expert object                                             |
  //+------------------------------------------------------------------+
  CExpert ExtExpert;
  
  //+------------------------------------------------------------------+
  //| Initialization function of the expert                            |
  //+------------------------------------------------------------------+
  int OnInit()
  {
    // Initializing expert
    if(!ExtExpert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber))
    {
      printf(__FUNCTION__ + ": error initializing expert");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    // Creating signal
    CExpertSignal *signal = new CExpertSignal;
    if(signal == NULL)
    {
      printf(__FUNCTION__ + ": error creating signal");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    
    ExtExpert.InitSignal(signal);
    signal.ThresholdOpen(Signal_ThresholdOpen);
    signal.ThresholdClose(Signal_ThresholdClose);
    signal.PriceLevel(Signal_PriceLevel);
    signal.StopLevel(Signal_StopLevel);
    signal.TakeLevel(Signal_TakeLevel);
    signal.Expiration(Signal_Expiration);
    
    // Creating filter Signal2MACross
    Signal2MACross *filter0 = new Signal2MACross;
    if(filter0 == NULL)
    {
      printf(__FUNCTION__ + ": error creating filter0");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    signal.AddFilter(filter0);
    
    // Set filter parameters
    filter0.SlowPeriod(Signal_2MACross_SlowPeriod);
    filter0.FastPeriod(Signal_2MACross_FastPeriod);
    filter0.MAMethod(Signal_2MACross_MAMethod);
    filter0.MAPrice(Signal_2MACross_MAPrice);
    filter0.Shift(Signal_2MACross_Shift);
    filter0.Weight(Signal_2MACross_Weight);
  
    ...
    
    // Check all trading objects parameters
    if(!ExtExpert.ValidationSettings())
    {
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
    
    // Tuning of all necessary indicators
    if(!ExtExpert.InitIndicators())
    {
      printf(__FUNCTION__ + ": error initializing indicators");
      ExtExpert.Deinit();
      return(INIT_FAILED);
    }
  
    return(INIT_SUCCEEDED);
  }

Полный код MA2Cross.mq5 прилагается. Все готово для компиляции, проверки в тестере и даже оптимизации на любом символе, включая и наш пользовательский ренко. А поскольку именно ренко станет далее полигоном, отвлечемся на минуту для пояснения некоторых связанных с ренко нюансов.

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

Заметьте, что 0-й бар ренко — всегда незавершен (во многих случаях, это даже свеча без "тела", не прямоугольник), поэтому сигнал нужно брать с 1-го бара. Это достигается за счет значения параметра Expert_EveryTick равного false.

Сгенерируем пользовательский ренко с размером бокса 100 пунктов на основе EURUSD. В результате получим символ EURUSD_T_r100. Выберем его в тестере, обязательно с таймфреймом M1.

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

Результат стратегии пересечения двух MA (MA2CrossCustom) на графике ренко 100 пунктов, производного от EURUSD

Результат стратегии пересечения двух MA (MA2CrossCustom) на графике ренко 100 пунктов, производного от EURUSD

В целях сравнения торговли по пользовательскому и реальному символам здесь приведен отчет эксперта MA2CrossCustom, который полностью аналогичен MA2Cross при пустом параметре WorkSymbol. Как получить MA2CrossCustom из MA2Cross — мы узнаем в следующем разделе.

В таблице сделок видно, что они проводятся по ценам, кратным размеру бокса: цены продаж соответствуют полностью, а цены покупок отличаются на размер спреда (наш генератор ренко сохраняет в каждый бокс максимальное значение спреда за время его формирования желающие могут изменить это поведение в исходном коде). Ренко строится по типу цены исходного графика, в данном случае bid. Для биржевых инструментов будет использоваться last.

Таблица сделок при торговле пользовательским ренко-символом на базе EURUSD

Таблица сделок при торговле пользовательским ренко-символом на базе EURUSD

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

Торговля в тестере по ренко-символу сказывается на точности результатов в любом режиме — по ценам открытия, M1 OHLC и по тикам.

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

Цена закрытия также не соответствует времени закрытия, потому что бар ренко — это бар М1, т.е. он имеет фиксированную длительность 1 минута.

Возможно генерировать нестандартное ренко, в котором бары маркируются временем, когда они завершаются, а не начинаются. Тогда цена закрытия будет соответствовать времени закрытия. Однако время открытия будет на 1 минуту раньше закрытия и потому не соответствовать реальной цене открытия (это цена закрытия плюс/минус размер ренко).

Анализ ренко предполагается проводить по сформированным барам, но их характеристическая цена — цена закрытия, а тестер при побаровой работе предоставляет для текущего (последнего) бара только цену открытия (режима по ценам закрытия нет). При этом и цены открытия боксов — по определению являются предикторами. Если брать сигналы индикаторов с закрытых баров (обычно, с 1-го), сделки в любом случае совершаются по текущей цене 0-го бара. И даже если обратиться к тиковым режимам, тестер генерирует тики для ренко по обычным правилам, руководствуясь опорными точками на основе конфигурации каждого бара. Тестер не учитывает особенностей строения и поведения ренко-котировок (которые мы пытаемся визуально эмулировать барами M1). Если гипотетически представить себе одномоментное формирование бара целиком, у него все равно есть тело, а для таких баров тестер генерирует тики, начиная с цены открытия. Если же задать бару тиковый объем, равный единице, то бар потеряет свою конфигурацию (станет ценовой меткой с равными OHLC).

Таким образом, все способы построения ренко будут иметь артефакты исполнения ордеров при тестировании по пользовательскому символу ренко.

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

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

Ренко при этом обеспечивает аналитику и тайминг — когда именно входить в рынок.

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

Адаптация советников для торговли на графиках пользовательских символов

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

К сожалению, имя исходного символа и созданного на его основе ренко никак нельзя связать средствами самой платформы. Было бы удобно иметь среди свойств пользовательского символа строковое поле "origin" (источник) или "parent" (родитель), в которое мы могли бы записать имя реального рабочего инструмента. По умолчанию оно было бы пустым, но если его заполнить, то платформа могла бы автоматически и прозрачно для пользователя подменять символ во всех торговых приказах и запросах истории. Поскольку данный механизм отсутствует в платформе, нам потребуется реализовать его самостоятельно. А задавать соответствие имен исходного и пользовательского символов придется с помощью параметров. В принципе, среди свойств пользовательских символов есть подходящее по смыслу поле 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(request.type == ORDER_TYPE_BUY
          || request.type == ORDER_TYPE_SELL)
          {
            if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_ASK)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK);
            if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_BID)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID);
          }
        }
      }
      
    public:
      static void setReplacementSymbol(const string replacementSymbol)
      {
        workSymbol = replacementSymbol;
      }
      
      static bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
      {
        replaceRequest(request);
        return ::OrderSend(request, result);
      }
      
      static bool OrderCalcProfit(ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit)
      {
        if(symbol == _Symbol && workSymbol != NULL)
        {
          symbol = workSymbol;
        }
        return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit);
      }
      
      static string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id)
      {
        const string result = ::PositionGetString(property_id);
        if(property_id == POSITION_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id)
      {
        const string result = ::OrderGetString(property_id);
        if(property_id == ORDER_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string HistoryOrderGetString(ulong ticket_number, ENUM_ORDER_PROPERTY_STRING property_id)
      {
        const string result = ::HistoryOrderGetString(ticket_number, property_id);
        if(property_id == ORDER_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static string HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id)
      {
        const string result = ::HistoryDealGetString(ticket_number, property_id);
        if(property_id == DEAL_SYMBOL && result == workSymbol) return _Symbol;
        return result;
      }
      
      static bool PositionSelect(string symbol)
      {
        if(symbol == _Symbol && workSymbol != NULL) return ::PositionSelect(workSymbol);
        return ::PositionSelect(symbol);
      }
      
      static string PositionGetSymbol(int index)
      {
        const string result = ::PositionGetSymbol(index);
        if(result == workSymbol) return _Symbol;
        return result;
      }
      ...
  };
  
  static string CustomOrder::workSymbol = NULL;

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

  bool CustomOrderSend(const MqlTradeRequest &request, MqlTradeResult &result)
  {
    return CustomOrder::OrderSend((MqlTradeRequest)request, result);
  }
  
  #define OrderSend CustomOrderSend

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

  #include <CustomOrder.mqh>
  #include <Expert\Expert.mqh>
  ...
  input string WorkSymbol = "";
  
  int OnInit()
  {
    if(WorkSymbol != "")
    {
      CustomOrder::setReplacementSymbol(WorkSymbol);
      
      // force a chart for the work symbol to open (in visual mode only)
      MqlRates rates[1];
      CopyRates(WorkSymbol, PERIOD_H1, 0, 1, rates);
    }
    ...
  }

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

Модифицированный указанным способом эксперт MA2Cross переименован в MA2CrossCustom.mq5.

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

Результат стратегии пересечения двух MA (MA2CrossCustom) на графике ренко 100 пунктов при торговле настоящим EURUSD

Результат стратегии пересечения двух MA (MA2CrossCustom) на графике ренко 100 пунктов при торговле настоящим EURUSD

По сравнению с первым тестом на символе ренко данный результат более реалистичный.

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

Сделки EURUSD при торговле эксперта с графика ренко-символа, производного от EURUSD

Сделки EURUSD при торговле эксперта с графика ренко-символа, производного от EURUSD

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

При торговле онлайн указанной проблемы не существует.

Проверим работоспособность CustomOrder на другом эксперте, написанном без применения стандартной библиотеки. Возьмем для этих целей эксперт ExprBot из статьи про Вычисление математических выражений — он также использует стратегию с пересечениями двух скользящих средних, но торговые операции выполняет с помощью библиотеки MT4Orders. Модифицированный эксперт ExprBotCustom.mq5 прилагается к статье вместе с требуемыми заголовочными файлами (папка ExpresSParserS).

В том же диапазоне дат 2019-2020 (первая половина года), с теми же настройками (периоды 7/11, тип скользящей средней LWMA, по ценам CLOSE на 1-м баре) получим следующие результаты.

Результат стратегии пересечения двух MA (ExprBotCustom) на графике ренко 100 пунктов, производном от EURUSD

Результат стратегии пересечения двух MA (ExprBotCustom) на графике ренко 100 пунктов, производном от EURUSD

Результат стратегии пересечения двух MA (ExprBotCustom) на графике ренко 100 пунктов при торговле настоящим EURUSD

Результат стратегии пересечения двух MA (ExprBotCustom) на графике ренко 100 пунктов при торговле настоящим EURUSD

Они очень похожи на те, что были получены с помощью эксперта MA2CrossCustom.

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

Заключение

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

Разумеется, это далеко не полный перечень возможностей, которые предоставляют пользовательские символы. Потенциальные области применения данной технологии гораздо шире. Например, можно вспомнить синтетические инструменты, дельту объемов, сторонние источники данных. Все это становится доступным на стандартных графиках MetaTrader 5, в тестере и онлайн, с помощью описанных программных преобразований.