Добавление, замена и удаление тиков

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

Для добавления тиков существует 2 функции CustomTicksAdd и CustomTicksReplace. Первая производит добавление интерактивных тиков, которые поступают в окно Обзора рынка (и оттуда они автоматически переносятся терминалом в базу тиков) и генерируют соответствующие события в MQL-программах. Вторая — записывает тики напрямую в базу тиков.

int CustomTicksAdd(const string symbol, const MqlTick &ticks[], uint count = WHOLE_ARRAY)

Функция CustomTicksAdd добавляет в ценовую историю пользовательского инструмента под именем symbol данные из массива ticks. По умолчанию, если параметр count равен WHOLE_ARRAY, добавляется весь массив, но при необходимости можно указать меньшее количество и загрузить лишь часть тиков.

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

Массив тиковых данных должен быть упорядочен по времени в порядке возрастания, то есть требуется, чтобы выполнялось условия ticks[i].time_msc <= ticks[j].time_msc для всех i < j.

Функция возвращает количество добавленных тиков либо -1 в случае ошибки.

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

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

Структура MqlTick имеет два поля со значением времени: time (время тика в секундах) и time_msc (время тика в миллисекундах). Оба значения ведут отсчет от 01 января 1970 года. Заполненное (ненулевое) поле time_msc имеет приоритет перед time. При этом time заполняется в секундах в результате пересчета по формуле time_msc / 1000. Если поле time_msc равно нулю, используется время из поля time, причем поле time_msc в свою очередь получает значение в миллисекундах из формулы time * 1000. Если оба поля равны нулю, в тик проставляется текущее время сервера (с точностью до миллисекунд).

Из двух полей, описывающих объем, volume_real имеет высший приоритет по сравнению с volume.

В зависимости от того, какие другие поля заполнены в конкретном элементе массива (структуре MqlTick), система устанавливает для сохраняемого тика флаги в поле flags:

  • ticks[i].bid — TICK_FLAG_BID (тик изменил цену Bid)
  • ticks[i].ask — TICK_FLAG_ASK (тик изменил цену Ask)
  • ticks[i].last — TICK_FLAG_LAST (тик изменил цену последней сделки)
  • ticks[i].volume или ticks[i].volume_real — TICK_FLAG_VOLUME (тик изменил объем)

Если значение какого-то поля меньше или равно нулю, соответствующий ему флаг не записываются в поле flags.

Флаги TICK_FLAG_BUY и TICK_FLAG_SELL в историю пользовательского инструмента не добавляются.

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

int CustomTicksReplace(const string symbol, long from_msc, long to_msc,
  const MqlTick &ticks[], uint count = WHOLE_ARRAY)

Интервал задается параметрами from_msc и to_msc, в миллисекундах с 01.01.1970. Оба значения входят в интервал.

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

Параметр count позволяет обрабатывать не весь массив, а лишь его часть.

Замена тиков производится последовательно день за днём до времени указанного в to_msc либо до возникновения ошибки в очередности тиков. Сначала обрабатывается первый день из указанного диапазона, затем следующий, и так далее. Как только обнаружится несоответствие времени тика порядку возрастания (неубывания), то процесс замены тиков прекращается на текущем дне. При этом тики за предыдущие дни будут успешно заменены, а текущий день (на момент неправильного тика) и все оставшиеся дни в указанном интервале останутся без изменения. Функция вернет -1, причем код ошибки в _LastError равен 0 ("нет ошибки").

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

Если в базе тиков в указанном интервале времени данные отсутствуют, то CustomTicksReplace просто добавит в нее тики из массива ticks.  

Удалить все тики в указанном временном интервале позволяет функция CustomTicksDelete.

int CustomTicksDelete(const string symbol, long from_msc, long to_msc)

Имя редактируемого пользовательского инструмента задается в параметре symbol, а очищаемый интервал — параметрами from_msc и to_msc (включительно), в миллисекундах.

Функция возвращает количество удаленных тиков либо -1 в случае ошибки.

Внимание! Удаление тиков с помощью CustomTicksDelete приводит к автоматическому удалению соответствующих баров! Однако вызов CustomRatesDelete, то есть удаление баров, не удаляет тики!

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

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

Ускорение тестирования и оптимизации
 
Трейдеры часто задаются вопросом, каким образом можно ускорить оптимизацию и тестирование экспертов. Среди возможных решений есть очевидные, для которых достаточно изменить настройки (когда это допустимо), а есть более трудоемкие, которые требуют адаптации эксперта или тестовой среды.
 
Среди первого класса решений можно отметить:
 
· уменьшение пространства оптимизации за счет исключения некоторых параметров или уменьшения их шага;
· уменьшение срока оптимизации;
· переход на режим моделирования тиков более низкого качества (например, от реальных к OHLC M1);
· включение опции расчета прибыли в пунктах, вместо денег;
· апгрейд компьютера;
· использование MQL Cloud или дополнительных компьютеров локальной сети.
 
Среди второго класса решений, связанных с разработкой, упомянем:
 
· профилировку кода, на основе которой можно ликвидировать "узкие" места в коде;
· по возможности, использовать экономный расчет индикаторов — без директивы #property tester_everytick_calculate;
· перенос алгоритмов индикаторов (если они используются) непосредственно в код советника: вызовы индикаторов налагают определенные накладные расходы;
· исключение работы с графикой и объектами;
· кэширование расчетов, если возможно;
· уменьшение количества одновременно открытых позиций и выставленных ордеров (их обсчет на каждом тике может стать заметным при большом числе);
· полная виртуализация расчетов, ордеров, сделок и позиций: встроенный механизм денежного учета в силу своей универсальности, поддержки мультивалютности и прочих особенностей имеет свои накладные расходы, которые можно исключить, выполняя аналогичные действия в коде на MQL5 (хотя этот вариант наиболее трудоемок);
 
Прореживание тиков относится к промежуточному классу решений: оно требует программного создания пользовательского символа, но зато не затрагивает исходный код эксперта.

Пользовательский символ с прореженными тиками будет создаваться скриптом CustomSymbolFilterTicks.mq5. Исходным инструментом будет выступать рабочий символ графика, на котором запускается скрипт. Во входных параметрах можно указать папку для пользовательского символа и начальную дату обработки истории. По умолчанию, если дата не задана, делается расчет для последних 120 дней.

input string CustomPath = "MQL5Book\\Part7"// Custom Symbol Folder
input datetime _Start;                       // Start (default: 120 days back)

Название символа формируется из имени исходного инструмента и суффикса ".TckFltr". Позднее мы добавим к нему обозначение метода прореживания тиков.

string CustomSymbol = _Symbol + ".TckFltr";
const uint DailySeconds = 60 * 60 * 24;
datetime Start = _Start == 0 ? TimeCurrent() - DailySeconds * 120 : _Start;

Для удобства пользователя, в обработчике OnStart предусмотрена возможность удалить предыдущую копию символа, если она уже существует.

void OnStart()
{
   bool custom = false;
   if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
   {
      if(IDYES == MessageBox(StringFormat("Delete existing custom symbol '%s'?"CustomSymbol),
         "Please, confirm"MB_YESNO))
      {
         SymbolSelect(CustomSymbolfalse);
         CustomRatesDelete(CustomSymbol0LONG_MAX);
         CustomTicksDelete(CustomSymbol0LONG_MAX);
         CustomSymbolDelete(CustomSymbol);
      }
      else
      {
         return;
      }
   }

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

   if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?"CustomSymbol),
      "Please, confirm"MB_YESNO))
   {
      if(PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         CustomSymbolSetString(CustomSymbolSYMBOL_DESCRIPTION"Prunned ticks by " + EnumToString(Mode));
         if(GenerateTickData())
         {
            SymbolSelect(CustomSymboltrue);
            ChartOpen(CustomSymbolPERIOD_H1);
         }
      }
   }
}

Функция GenerateTickData обрабатывает тики в цикле порциями, по суткам. Тики за сутки запрашиваются с помощью вызова CopyTicksRange. Далее их надо тем или иным способом проредить, что делегировано классу TickFilter, который мы покажем ниже. Наконец, массив тиков добавляется в историю пользовательского символа с помощью CustomTicksReplace.

bool GenerateTickData()
{
   bool result = true;
   datetime from = Start / DailySeconds * DailySeconds// округляем до начала суток
   ulong read = 0written = 0;
   uint day = 0;
   const uint total = (uint)((TimeCurrent() - from) / DailySeconds + 1);
   MqlTick array[];
   
   while(!IsStopped() && from < TimeCurrent())
   {
      Comment(TimeToString(fromTIME_DATE), " "day++, "/"total);
      
      const int r = CopyTicksRange(_SymbolarrayCOPY_TICKS_ALL,
         from * 1000L, (from + DailySeconds) * 1000L - 1);
      if(r < 0)
      {
         Alert("Error reading ticks at "TimeToString(fromTIME_DATE));
         result = false;
         break;
      }
      read += r;
      
      if(r > 0)
      {
         const int t = TickFilter::filter(Modearray);
         const int w = CustomTicksReplace(CustomSymbol,
            from * 1000L, (from + DailySeconds) * 1000L - 1array);
         if(w <= 0)
         {
            Alert("Error writing custom ticks at "TimeToString(fromTIME_DATE));
            result = false;
            break;
         }
         written += w;
      }
      from += DailySeconds;
   }
   
   if(read > 0)
   {
      PrintFormat("Done ticks - read: %lld, written: %lld, ratio: %.1f%%",
         readwrittenwritten * 100.0 / read);
   }
   Comment("");
   return result;
}

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

Теперь обратимся непосредственно к методике прореживания тиков. Очевидно, что подходов может быть много, и каждый лучше или хуже подойдет к конкретной торговой стратегии. Мы предложим 3 базовых варианта, объединенных в классе TickFilter (TickFilter.mqh). Также, для полноты картины, там поддержан и режим копирования тиков без прореживания.

Таким образом, в классе реализованы следующие режимы:

  • без прореживания;
  • пропуск последовательностей тиков с монотонным изменением цены без разворота (а-ля "зиг-заг");
  • пропуск колебаний цен в пределах спреда;
  • запись только тиков с фрактальной конфигурацией, когда цена Bid или Ask представляет собой экстремум между двумя соседними тиками.

Данные режимы описаны в виде элементов перечисления FILTER_MODE.

class TickFilter
{
public:
   enum FILTER_MODE
   {
      NONE,
      SEQUENCE,
      FLUTTER,
      FRACTALS,
   };
   ...

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

   static int filterBySequences(MqlTick &data[]);
   static int filterBySpreadFlutter(MqlTick &data[]);
   static int filterByFractals(MqlTick &data[]);

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

Для унификации выполнения процедуры в разных режимах предусмотрен метод filter. Для режима NONE массив data просто остается без изменений.

   static int filter(FILTER_MODE modeMqlTick &data[])
   {
      switch(mode)
      {
      case SEQUENCEreturn filterBySequences(data);
      case FLUTTERreturn filterBySpreadFlutter(data);
      case FRACTALSreturn filterByFractals(data);
      }
      return ArraySize(data);
   }

Например, вот как реализована фильтрация по монотонным последовательностям тиков в методе filterBySequences.

   static int filterBySequences(MqlTick &data[])
   {
      const int size = ArraySize(data);
      if(size < 3return size;
      
      int index = 2;
      bool dirUp = data[1].bid - data[0].bid + data[1].ask - data[0].ask > 0;
      
      for(int i = 2i < sizei++)
      {
         if(dirUp)
         {
            if(data[i].bid - data[i - 1].bid + data[i].ask - data[i - 1].ask < 0)
            {
               dirUp = false;
               data[index++] = data[i];
            }
         }
         else
         {
            if(data[i].bid - data[i - 1].bid + data[i].ask - data[i - 1].ask > 0)
            {
               dirUp = true;
               data[index++] = data[i];
            }
         }
      }
      return ArrayResize(dataindex);
   }

А вот как выглядит прореживание по фракталам.

   static int filterByFractals(MqlTick &data[])
   {
      int index = 1;
      const int size = ArraySize(data);
      if(size < 3return size;
      
      for(int i = 1i < size - 2i++)
      {
         if((data[i].bid < data[i - 1].bid && data[i].bid < data[i + 1].bid)
         || (data[i].ask > data[i - 1].ask && data[i].ask > data[i + 1].ask))
         {
            data[index++] = data[i];
         }
      }
      
      return ArrayResize(dataindex);
   }

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

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

   Create new custom symbol 'EURUSD.TckFltr-SE'?
   Fixing SYMBOL_TRADE_TICK_VALUE: 0.0 <<< 1.0
   true  SYMBOL_TRADE_TICK_VALUE 1.0 -> SUCCESS (0)
   Fixing SYMBOL_TRADE_TICK_SIZE: 0.0 <<< 1e-05
   true  SYMBOL_TRADE_TICK_SIZE 1e-05 -> SUCCESS (0)
   Number of found discrepancies: 2
   Fixed
   Done ticks - read: 31553509, written: 16927376, ratio: 53.6%

Для режимов сглаживания колебаний и по фракталам показатели другие:

   EURUSD.TckFltr-FL will be updated
   Done ticks - read: 31568782, written: 22205879, ratio: 70.3%
   ...   
   Create new custom symbol 'EURUSD.TckFltr-FR'?
   ...
   Done ticks - read: 31569519, written: 12732777, ratio: 40.3%

Для практических торговых экспериментов на основе прореженных тиков нам потребуется эксперт. Возьмем адаптированную версию BandOsMAticks.mq5, в которой по сравнению с оригиналом включена торговля на каждом тике (в методе SimpleStrategy::trade отключена строка if(lastBar == iTime(_Symbol, _Period, 0)) return false;), а значения сигнальных индикаторов берутся с баров 0 и 1 (раньше были только завершенные бары 1 и 2).

Запустим эксперт на диапазоне дат с начала 2021 года по 1 июня 2022. Настройки прилагаются в файле MQL5/Presets/MQL5Book/BandOsMAticks.set. Общее поведение кривой баланса во всех режимах достаточно схожее.

Совмещенные графики балансов тестов в разных режимах по тикам

Совмещенные графики балансов тестов в разных режимах по тикам

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

Различия в показателях приведены в следующей таблице (N — количество трейдов, $ — прибыль, PF — профит-фактор, RF — фактор восстановления, DD — просадка):

Режим

Тики

Время
mm:ss.msec

Память

N

$

PF

RF

DD

Реальные

31002919

02:45.251

835 Mb

962

166.24

1.32

2.88

54.99

Эмуляция

25808139

01:58.131

687 Mb

928

171.94

1.34

3.44

47.64

OHLC M1

2084820

00:11.094

224 Mb

856

193.52

1.39

3.97

46.55

Sequence

16310236

01:24.784

559 Mb

860

168.95

1.34

2.92

55.16

Flutter

21362616

01:52.172

623 Mb

920

179.75

1.37

3.60

47.28

Fractal

12270854

01:04.756

430 Mb

866

142.19

1.27

2.47

54.80

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

Среди трех режимов с искусственно прореженными тиками наиболее близким к реальному по комплексу показателей является Sequence. По времени он в 2 раза быстрее реального, а по памяти в 1.5 раза экономичнее. Режим Flutter, судя по всему, лучше сохраняет оригинальное количество сделок. Наиболее быстрый и наименее требовательный к памяти режим по фракталам, конечно, проигрывает OHLC M1, но зато не завышает торговые оценки.

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

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

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

Попробуем решить эти проблемы собственными силами.

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

Данный подход имеет несколько преимуществ:

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

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

Также потенциально интересна модификация цен на лету (увеличение спреда, например).

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

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

input string CustomPath = "MQL5Book\\Part7"// Custom Symbol Folder
input datetime _Start;                       // Start (по умолчанию: отступ на 120 дней)
input ENUM_TIMEFRAMES Timeframe = PERIOD_H1;

Имя нового символа конструируется из имени символа текущего графика и суффикса ".Tester".

string CustomSymbol = _Symbol + ".Tester";

Если начальная дата в параметрах не задана, эксперт сделает отступ назад на 120 дней от текущей даты.

const uint DailySeconds = 60 * 60 * 24;
datetime Start = _Start == 0 ? TimeCurrent() - DailySeconds * 120 : _Start;

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

bool FirstCopy = true;
// дополнительно 1 день назад, потому что иначе график не сразу обновится
datetime Cursor = (Start / DailySeconds - 1) * DailySeconds// округляем по границе суток

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

MqlTick Ticks[];       // тики для "текущего" дня в прошлом
int Index = 0;         // позиция в тиках внутри дня
int Step = 32;         // перемотка вперед по 32 тика за раз (по умолчанию)
int StepRestore = 0;   // запоминаем скорость на время паузы
long Chart = 0;        // созданный график кастом-символа
bool InitDone = false// признак завершенной инициализации

Для воспроизведения тиков с постоянной скоростью запустим таймер в OnInit.

void OnInit()
{
   EventSetMillisecondTimer(100);
}
   
void OnTimer()
{
   if(!GenerateData())
   {
      EventKillTimer();
   }
}

Генерацию тиков поручим функции GenerateData. Сразу после запуска, когда флаг InitDone сброшен, попытаемся создать новый символ или очистить прежние котировки и тики, если кастом-символ уже существует.

bool GenerateData()
{
   if(!InitDone)
   {
      bool custom = false;
      if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
      {
         if(IDYES == MessageBox(StringFormat("Clean up existing custom symbol '%s'?",
            CustomSymbol), "Please, confirm"MB_YESNO))
         {
            PRTF(CustomRatesDelete(CustomSymbol0LONG_MAX));
            PRTF(CustomTicksDelete(CustomSymbol0LONG_MAX));
            Sleep(1000);
            MqlRates rates[1];
            MqlTick tcks[];
            if(PRTF(CopyRates(CustomSymbolPERIOD_M101rates)) == 1
            || PRTF(CopyTicks(CustomSymboltcks) > 0))
            {
               Alert("Can't delete rates and Ticks, internal error");
               ExpertRemove();
            }
         }
         else
         {
            return false;
         }
      }
      else
      if(!PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         return false;
      }
      ... // (A)

На данном этапе мы кое-что опустим в месте (A) и вернемся к данному моменту позднее.

После создания символа, выбираем его в Обзор рынка и открываем для него чарт.

 SymbolSelect(CustomSymboltrue);
      Chart = ChartOpen(CustomSymbolTimeframe);
      ... // (B)
      ChartSetString(ChartCHART_COMMENT"Custom Tester");
      ChartSetInteger(ChartCHART_SHOW_OBJECT_DESCRtrue);
      ChartRedraw(Chart);
      InitDone = true;
   }
   ...

Здесь тоже пропущена пара строк (B), связанных с будущими усовершенствованиями, но пока они не требуются.

Если символ уже создан, запускаем трансляцию тиков пакетами Step тиков, но не более 256. Это ограничение связано с особенностью функции CustomTicksAdd.

   else
   {
      for(int i = 0i <= (Step - 1) / 256; ++i)
      if(Step > 0 && !GenerateTicks())
      {
         return false;
      }
   }
   return true;
}

Вспомогательная функция GenerateTicks транслирует тики порциями по Step тиков (но не более 256), считывая их из суточного массива Ticks по смещению Index. Когда массив пуст или мы прочитали его до конца, запрашиваем тики следующего дня путем вызова FillTickBuffer.

bool GenerateTicks()
{
   if(Index >= ArraySize(Ticks)) // суточный массив пуст или прочитан до конца
   {
      if(!FillTickBuffer()) return false// заполняем массив тиками за сутки
   }
   
   const int m = ArraySize(Ticks);
   MqlTick array[];
   const int n = ArrayCopy(arrayTicks0Indexfmin(fmin(Step256), m));
   if(n <= 0return false;
   
   ResetLastError();
   if(CustomTicksAdd(CustomSymbolarray) != ArraySize(array) || _LastError != 0)
   {
      Print(_LastError); // на случай ERR_CUSTOM_TICKS_WRONG_ORDER (5310)
      ExpertRemove();
   }
   Comment("Speed: ", (string)Step" / "STR_TIME_MSC(array[n - 1].time_msc));
   Index += Step// перемещаемся на Step тиков вперед
   return true;
}

Функция FillTickBuffer использует в своей работе CopyTicksRange.

bool FillTickBuffer()
{
   int r;
   ArrayResize(Ticks0);
   do
   {
      r = PRTF(CopyTicksRange(_SymbolTicksCOPY_TICKS_ALLCursor * 1000L,
         (Cursor + DailySeconds) * 1000L - 1));
      if(r > 0 && FirstCopy)
      {
         // NB: этот предварительный вызов нужен только, чтобы вывести график
          // из состояния "Ожидания Обновления"
         PRTF(CustomTicksReplace(CustomSymbolCursor * 1000L,
            (Cursor + DailySeconds) * 1000L - 1Ticks));
         FirstCopy = false;
         r = 0;
      }
      Cursor += DailySeconds;
   }
   while(r == 0 && Cursor < TimeCurrent()); // пропускаем неторговые дни
   Index = 0;
   return r > 0;
}

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

void OnDeinit(const int)
{
   if(Chart != 0)
   {
      ChartClose(Chart);
   }
   Comment("");
}

На этом эксперт можно было бы считать завершенным, однако существует одна проблема. Дело в том, что по тем или иным причинам свойства кастом-символа не копируются один в один из исходного рабочего символа, по крайней мере, в текущей реализации MQL5 API. Это касается даже очень важных свойств, таких как SYMBOL_TRADE_TICK_VALUE, SYMBOL_TRADE_TICK_SIZE. Если мы выведем на печать значения этих свойств сразу после вызова CustomSymbolCreate(CustomSymbol, CustomPath, _Symbol), то увидим там нули.

Чтобы организовать проверку свойств, их сравнение и при необходимости исправление был написан специальный класс CustomSymbolMonitor (CustomSymbolMonitor.mqh), производный от SymbolMonitor. С его внутренним устройством предлагается разобраться самостоятельно, а здесь мы лишь приведем публичный интерфейс.

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

class CustomSymbolMonitorpublic SymbolMonitor
{
public:
   CustomSymbolMonitor(); // образец - _Symbol
   CustomSymbolMonitor(const string sconst SymbolMonitor *m = NULL);
   CustomSymbolMonitor(const string sconst string other);
   
   // установить/заменить символ-образец   
   void inherit(const SymbolMonitor &m);
   
   // скопировать все свойства из символа-образца в прямом или обратном порядке   
   bool setAll(const bool reverseOrder = trueconst int limit = UCHAR_MAX);
   
   // сверить все свойства с образцом, вернуть количество исправлений   
   int verifyAll(const int limit = UCHAR_MAX);
   
   // сверить указанные свойства с образцом, вернуть количество исправлений   
   int verify(const int &properties[]);
   
   // скопировать заданные свойства из образца, вернуть true если все они применились   
   bool set(const int &properties[]);
   
   // скопировать конкретное свойство из образца, вернуть true если оно применилось   
   template<typename E>
   bool set(const E e);
   
   bool set(const ENUM_SYMBOL_INFO_INTEGER propertyconst long valueconst
   {
      return CustomSymbolSetInteger(namepropertyvalue);
   }
   
   bool set(const ENUM_SYMBOL_INFO_DOUBLE propertyconst double valueconst
   {
      return CustomSymbolSetDouble(namepropertyvalue);
   }
   
   bool set(const ENUM_SYMBOL_INFO_STRING propertyconst string valueconst
   {
      return CustomSymbolSetString(namepropertyvalue);
   }
};

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

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

      // (A) проверяем важные свойства и устанавливаем их в "ручном" режиме
      SymbolMonitor sm// _Symbol
      CustomSymbolMonitor csm(CustomSymbol, &sm);
      int props[] = {SYMBOL_TRADE_TICK_VALUESYMBOL_TRADE_TICK_SIZE};
      const int d1 = csm.verify(props); // проверяем и пытаемся исправить
      if(d1)
      {
         Print("Number of found descrepancies: "d1); // число исправлений
         if(csm.verify(props)) // проверяем еще раз
         {
            Alert("Custom symbol can not be created, internal error!");
            return false// без успешных правок использовать символ нельзя
         }
         Print("Fixed");
      }

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

Однако это делается с постоянной скоростью 32 тика за 0.1 секунды. Желательно на лету менять скорость воспроизведения по желанию пользователя, как в большую, так и в меньшую сторону. Такое управление можно организовать, например, с клавиатуры.

Следовательно, требуется добавить обработчик OnChartEvent. Как мы знаем, для события CHARTEVENT_KEYDOWN в программу поступает код нажатой клавиши в параметре lparam, и мы передаем его в функцию CheckKeys (см. ниже). Некий фрагмент (C), тесно связанный с (B), пока пришлось отложить — мы к ним скоро вернемся.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   ... // (C)
   if(id == CHARTEVENT_KEYDOWN// эти события поступают только пока график активен!
   {
      CheckKeys(lparam);
   }
}

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

void CheckKeys(const long key)
{
   if(key == VK_DOWN)
   {
      Step /= 2;
      if(Step > 0)
      {
         Print("Slow down: "Step);
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
      else
      {
         Print("Paused");
         ChartSetString(ChartCHART_COMMENT"Paused");
         ChartRedraw(Chart);
      }
   }
   else if(key == VK_UP)
   {
      if(Step == 0)
      {
         Step = 1;
         Print("Resumed");
         ChartSetString(ChartCHART_COMMENT"Resumed");
      }
      else
      {
         Step *= 2;
         Print("Speed up: "Step);
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
   }
   else if(key == VK_PAUSE)
   {
      if(Step > 0)
      {
         StepRestore = Step;
         Step = 0;
         Print("Paused");
         ChartSetString(ChartCHART_COMMENT"Paused");
         ChartRedraw(Chart);
      }
      else
      {
         Step = StepRestore;
         Print("Resumed");
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
   }
}

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

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

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

Исходный код индикатора прилагается в файле KeyboardSpy.mq5. Разумеется, индикатор не имеет диаграмм. Пара входных параметров предназначена для получения идентификатора графика HostID, куда следует отправлять сообщения, и кода пользовательского события EventID, в которое будут "упаковываться" интерактивные события.

#property indicator_chart_window
#property indicator_plots 0
   
input long HostID;
input ushort EventID;

Основная "работа" выполняется в обработчике OnChartEvent — она элементарна.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      EventChartCustom(HostIDEventIDlparam,
         // здесь всегда 0, когда внутри iCustom
         (double)(ushort)TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL),
         sparam);
   }
}

Обратите внимание, что все выбранные нами "горячие клавиши" являются простыми, то есть не используют сочетаний с клавишами состояния клавиатуры, такими как Ctrl или Shift. Это сделано вынужденно, потому что внутри индикаторов, созданных программно (в частности, через iCustom), состояние клавиатуры не считывается. Иными словами, вызов TerminalInfoInteger(TERMINAL_KEYSTATE_XYZ) всегда возвращает 0. В вышеприведенном обработчике мы добавили его просто для демонстрации, чтобы вы могли при желании убедиться в данном ограничении, выведя поступающие параметры на "принимающей стороне".

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

В пропущенном ранее фрагменте (B), во время инициализации генератора, создадим индикатор и добавим его на график кастом-символа.

#define EVENT_KEY 0xDED // пользовательское событие
      ...
      // (B)
      const int handle = iCustom(CustomSymbolTimeframe"MQL5Book/p7/KeyboardSpy",
         ChartID(), EVENT_KEY);
      ChartIndicatorAdd(Chart0handle);

Далее во фрагменте (C) обеспечим прием пользовательских сообщений из индикатора и их передачу в уже известную функцию CheckKeys.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   // (C)
   if(id == CHARTEVENT_CUSTOM + EVENT_KEY// уведомления с зависимого чарта, когда он активен
   {
      CheckKeys(lparam); // "дистанционная" обработка нажатий клавиш
   }
   else if(id == CHARTEVENT_KEYDOWN// эти события поступают только пока график активен!
   {
      CheckKeys(lparam); // стандартная обработка
   }
}

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

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

На графике с экспертом в комментарий выводится время "текущих" транслируемых тиков.

Эксперт, воспроизводящий историю тиков (и котировок) реального символа

Эксперт, воспроизводящий историю тиков (и котировок) реального символа

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

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

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

  • 'B' — покупка по рынку;
  • 'S' — продажа по рынку;
  • 'U' — установка лимитного ордера на покупку;
  • 'L' — установка лимитного ордера на продажу;
  • 'C' — закрыть все позиции;
  • 'D' — удалить все ордера;
  • 'R' — вывести в журнал торговый отчет.

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

input double Volume;           // Volume (0 = minimal lot)
input int Distance2SLTP = 0;   // Distance to SL/TP in points (0 = no)
   
const double Lot = Volume == 0 ? SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;

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

С учетом ранее представленных классов из MqlTradeSync.mqh, вышеописанная логика преобразуется в следующий исходный код.

#include <MQL5Book/MqlTradeSync.mqh>
   
#define KEY_B 66
#define KEY_C 67
#define KEY_D 68
#define KEY_L 76
#define KEY_R 82
#define KEY_S 83
#define KEY_U 85
   
void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      MqlTradeRequestSync request;
      const double ask = SymbolInfoDouble(_SymbolSYMBOL_ASK);
      const double bid = SymbolInfoDouble(_SymbolSYMBOL_BID);
      const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   
      switch((int)lparam)
      {
      case KEY_B:
         request.buy(Lot0,
            Distance2SLTP ? ask - point * Distance2SLTP : Distance2SLTP,
            Distance2SLTP ? ask + point * Distance2SLTP : Distance2SLTP);
         break;
      case KEY_S:
         request.sell(Lot0,
            Distance2SLTP ? bid + point * Distance2SLTP : Distance2SLTP,
            Distance2SLTP ? bid - point * Distance2SLTP : Distance2SLTP);
         break;
      case KEY_U:
         if(Distance2SLTP)
         {
            request.buyLimit(Lot, ask - point * Distance2SLTP);
         }
         break;
      case KEY_L:
         if(Distance2SLTP)
         {
            request.sellLimit(Lot, bid + point * Distance2SLTP);
         }
         break;
      case KEY_C:
         for(int i = PositionsTotal() - 1i >= 0i--)
         {
            request.close(PositionGetTicket(i));
         }
         break;
      case KEY_D:
         for(int i = OrdersTotal() - 1i >= 0i--)
         {
            request.remove(OrderGetTicket(i));
         }
         break;
      case KEY_R:
         // тут что-то должно быть...
         break;
      }
   }
}

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

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

Экспериментальная реализация подхода продемонстрирована в прилагаемом файле CustomTrade.mqh. С полным кодом можно ознакомиться самостоятельно, а в рамках книги мы перечислим лишь основные моменты.

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

Весь код обернут в пространство имен CustomTrade для исключения конфликтов.

Сущности ордер, сделка и позиция формализованы в виде соответствующих классов CustomOrder, CustomDeal, CustomPosition. Все они являются наследниками класса MonitorInterface<I,D,S>::TradeState. Напомним, что в этом классе уже автоматически поддержано формирование массивов целочисленных, вещественных и строковых свойств для каждого типа объектов и его специфических троек перечислений. Например, CustomOrder выглядит так:

class CustomOrderpublic MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>::TradeState
{
   static long ticket// счетчик ордеров и постащик тикетов
   static int done;    // счетчик исполненных (исторических) ордеров
public:
   CustomOrder(const ENUM_ORDER_TYPE typeconst double volumeconst string symbol)
   {
      _set(ORDER_TYPEtype);
      _set(ORDER_TICKET, ++ticket);
      _set(ORDER_TIME_SETUPSymbolInfoInteger(symbolSYMBOL_TIME));
      _set(ORDER_TIME_SETUP_MSCSymbolInfoInteger(symbolSYMBOL_TIME_MSC));
      if(type <= ORDER_TYPE_SELL)
      {
         // TODO: пока нет отложенного исполнения
         setDone(ORDER_STATE_FILLED);
      }
      else
      {
         _set(ORDER_STATEORDER_STATE_PLACED);
      }
      
      _set(ORDER_VOLUME_INITIALvolume);
      _set(ORDER_VOLUME_CURRENTvolume);
      
      _set(ORDER_SYMBOLsymbol);
   }
   
   void setDone(const ENUM_ORDER_STATE state)
   {
      const string symbol = _get<string>(ORDER_SYMBOL);
      _set(ORDER_TIME_DONESymbolInfoInteger(symbolSYMBOL_TIME));
      _set(ORDER_TIME_DONE_MSCSymbolInfoInteger(symbolSYMBOL_TIME_MSC));
      _set(ORDER_STATEstate);
      ++done;
   }
   
   bool isActive() const
   {
      return _get<long>(ORDER_TIME_DONE) == 0;
   }
   
   static int getDoneCount()
   {
      return done;
   }
};

Обратите внимание, что в виртуальном окружении старого "текущего" времени нельзя использовать функцию TimeCurrent и вместо неё берется последнее известное время кастом-символа SymbolInfoInteger(symbol, SYMBOL_TIME).

В процессе виртуальной торговли текущие объекты и их история накапливается в массивах соответствующих классов.

AutoPtr<CustomOrderorders[];
CustomOrder *selectedOrders[];
CustomOrder *selectedOrder = NULL;
AutoPtr<CustomDealdeals[];
CustomDeal *selectedDeals[];
CustomDeal *selectedDeal = NULL;
AutoPtr<CustomPositionpositions[];
CustomPosition *selectedPosition = NULL;

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

#define HistorySelect CustomTrade::MT5HistorySelect
#define HistorySelectByPosition CustomTrade::MT5HistorySelectByPosition
#define PositionGetInteger CustomTrade::MT5PositionGetInteger
#define PositionGetDouble CustomTrade::MT5PositionGetDouble
#define PositionGetString CustomTrade::MT5PositionGetString
#define PositionSelect CustomTrade::MT5PositionSelect
#define PositionSelectByTicket CustomTrade::MT5PositionSelectByTicket
#define PositionsTotal CustomTrade::MT5PositionsTotal
#define OrdersTotal CustomTrade::MT5OrdersTotal
#define PositionGetSymbol CustomTrade::MT5PositionGetSymbol
#define PositionGetTicket CustomTrade::MT5PositionGetTicket
#define HistoryDealsTotal CustomTrade::MT5HistoryDealsTotal
#define HistoryOrdersTotal CustomTrade::MT5HistoryOrdersTotal
#define HistoryDealGetTicket CustomTrade::MT5HistoryDealGetTicket
#define HistoryOrderGetTicket CustomTrade::MT5HistoryOrderGetTicket
#define HistoryDealGetInteger CustomTrade::MT5HistoryDealGetInteger
#define HistoryDealGetDouble CustomTrade::MT5HistoryDealGetDouble
#define HistoryDealGetString CustomTrade::MT5HistoryDealGetString
#define HistoryOrderGetDouble CustomTrade::MT5HistoryOrderGetDouble
#define HistoryOrderGetInteger CustomTrade::MT5HistoryOrderGetInteger
#define HistoryOrderGetString CustomTrade::MT5HistoryOrderGetString
#define OrderSend CustomTrade::MT5OrderSend
#define OrderSelect CustomTrade::MT5OrderSelect
#define HistoryOrderSelect CustomTrade::MT5HistoryOrderSelect
#define HistoryDealSelect CustomTrade::MT5HistoryDealSelect

Вот, например, как реализована функция MT5HistorySelectByPosition.

bool MT5HistorySelectByPosition(long id)
{
   ArrayResize(selectedOrders0);
   ArrayResize(selectedDeals0);
  
   for(int i = 0i < ArraySize(orders); i++)
   {
      CustomOrder *ptr = orders[i][];
      if(!ptr.isActive())
      {
         if(ptr._get<long>(ORDER_POSITION_ID) == id)
         {
            PUSH(selectedOrdersptr);
         }
      }
   }
   
   for(int i = 0i < ArraySize(deals); i++)
   {
      CustomDeal *ptr = deals[i][];
      if(ptr._get<long>(DEAL_POSITION_ID) == id)
      {
         PUSH(selectedDealsptr);
      }
   }
   return true;

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

Вторая группа функций в пространстве CustomTrade выполняет утилитарные действия: проверяет и обновляет состояния ордеров, сделок и позиций, создает новые и удаляет старые объекты в соответствии с обстановкой. В частности, среди них есть функции CheckPositions и CheckOrders, которые можно вызывать по таймеру или в ответ на действия пользователя. Но это можно и не делать, если использовать пару других функций, предназначенных для отображения текущего и исторического состояния виртуального торгового счета:

  • string ReportTradeState() — возвращает многострочный текст со списком открытых позиций и выставленных ордеров;
  • void PrintTradeHistory() — выводит в журнал историю ордеров и сделок.

Эти функции самостоятельно вызывают CheckPositions и CheckOrders, чтобы предоставить вам актуальную информацию.

Кроме того имеется функция для визуализации позиций и действующих ордеров на графике в виде объектов: DisplayTrades.

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

#include <MQL5Book/CustomTrade.mqh>
#include <MQL5Book/MqlTradeSync.mqh>

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

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

int OnInit()
{
   EventSetTimer(1);
   return INIT_SUCCEEDED;
}
   
void OnTimer()
{
   Comment(CustomTrade::ReportTradeState());
   CustomTrade::DisplayTrades();
}

Для построения отчета по нажатию 'R' дополним обработчик OnChartEvent.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      switch((int)lparam)
      {
      ...
      case KEY_R:
         CustomTrade::PrintTradeHistory();
         break;
      }
   }
}

Наконец, все готово для проверки нового программного комплекса в действии.

Запустим генератор кастом-символа CustomTester.mq5 на EURUSD. На открывшемся графике "EURUSD.Tester" запустим CustomOrderSend.mq5 и начнем "торговать". Ниже показано изображение процесса тестирования.

Виртуальная торговля на графике пользовательского символа

Виртуальная торговля на графике пользовательского символа

Здесь видны 2 открытых длинных позиции (с защитными уровнями) и отложенный лимитный ордер на продажу.

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

Виртуальная торговля на графике пользовательского символа

Виртуальная торговля на графике пользовательского символа

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

History Orders:

(1) #1 ORDER_TYPE_BUY 2022.02.15 01:20:50 -> 2022.02.15 01:20:50 L=0.01 @ 1.1306 

(4) #2 ORDER_TYPE_SELL_LIMIT 2022.02.15 02:34:29 -> 2022.02.15 18:10:17 L=0.01 @ 1.13626 [sell limit]

(2) #3 ORDER_TYPE_BUY 2022.02.15 10:08:20 -> 2022.02.15 10:08:20 L=0.01 @ 1.13189 

(3) #4 ORDER_TYPE_BUY 2022.02.15 15:01:26 -> 2022.02.15 15:01:26 L=0.01 @ 1.13442 

(1) #5 ORDER_TYPE_SELL 2022.02.15 15:35:43 -> 2022.02.15 15:35:43 L=0.01 @ 1.13568 

(2) #6 ORDER_TYPE_SELL 2022.02.16 09:39:17 -> 2022.02.16 09:39:17 L=0.01 @ 1.13724 

(4) #7 ORDER_TYPE_BUY 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13748 

(3) #8 ORDER_TYPE_SELL 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13742 

Deals:

(1) #1 [#1] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 01:20:50 L=0.01 @ 1.1306 = 0.00 

(2) #2 [#3] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 10:08:20 L=0.01 @ 1.13189 = 0.00 

(3) #3 [#4] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 15:01:26 L=0.01 @ 1.13442 = 0.00 

(1) #4 [#5] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.15 15:35:43 L=0.01 @ 1.13568 = 5.08 [tp]

(4) #5 [#2] DEAL_TYPE_SELL DEAL_ENTRY_IN 2022.02.15 18:10:17 L=0.01 @ 1.13626 = 0.00 

(2) #6 [#6] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 09:39:17 L=0.01 @ 1.13724 = 5.35 [tp]

(4) #7 [#7] DEAL_TYPE_BUY DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13748 = -1.22 

(3) #8 [#8] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13742 = 3.00 

Total: 12.21, Trades: 4

В круглых скобках — идентификаторы позиций, в квадратных скобках — тикеты ордеров для соответствующих сделок (тикеты обоих типов предваряются "решёткой" '#').

Здесь не учитываются свопы и комиссии. Их расчет можно добавить.

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