English Español Deutsch 日本語 Português
preview
Разработка системы репликации (Часть 26): Проект советника — Класс C_Terminal

Разработка системы репликации (Часть 26): Проект советника — Класс C_Terminal

MetaTrader 5Тестер | 13 февраля 2024, 14:19
389 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации - Моделирование рынка (Часть 25): Подготовка к следующему этапу", мы подготовили репликацию/моделирование для базового использования. Несмотря на это, нам нужно нечто большее, чем просто анализ прошлых движений или потенциальных действий, нам необходимо что-то еще. А именно нам нужен инструмент, который позволит проводить целевые исследования, как если бы мы работали на реальном рынке. Для этого необходимо создать советник, позволяющий проводить более глубокие исследования. Кроме того, мы намерены разработать универсальный советник, применимый к различным рынкам (фондовым и валютным), а также совместимый с нашей системой репликации/моделирования.

Учитывая масштабы проекта, задача нам предстоит серьезная. Однако сложность разработки не столь высока, как кажется, поскольку большую часть процесса я уже рассказывал в предыдущих статьях. Среди них "Разработка торгового советника с нуля (Часть 31): Навстречу будущему (IV)" и "Как построить советник, работающий автоматически (Часть 15): Автоматизация (VII)", в этих статьях я подробно рассказал о разработке полностью автоматического советника. Несмотря на эти материалы, здесь мы сталкиваемся с уникальной и еще более сложной задачей: заставить платформу MetaTrader 5 имитировать подключение к торговому серверу, продвигая реалистичное моделирование открытого рынка. Задача, вне всякого сомнения, значительно сложная.

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


Концепции реализации советника

Возможно, вы уже заметили, что я большой поклонник объектно-ориентированного программирования (ООП). Это связано с широкими возможностями, которые предлагает ООП, а также оно с самого начала предоставляет способ написания кода, который делает его более устойчивым, безопасным и надежным. Для начала нам необходимо составить предварительное представление о том, что нам понадобится, организовав структуру проекта. Имея опыт и пользователя, и программиста, я понял, что для того, чтобы советник был по-настоящему эффективен, он должен использовать ресурсы, которые всегда доступны нам: клавиатура и мышь. Учитывая, что платформа MetaTrader 5 основана на графиках, то использование мыши для взаимодействия с графическими элементами имеет важное значение. Но клавиатура также играет ключевую роль для помощи в различных аспектах. Однако обсуждение не ограничивается использованием мыши и клавиатуры, что будет рассмотрено в серии об автоматизации. В некоторых случаях полная автоматизация обходится без данных средств, но при выборе их использования важно учитывать характер выполняемой операции. Таким образом, не все советники хорошо подходят для всех видов активов.

Так происходит, потому что некоторые из них демонстрируют изменение цен на 0,01, другие на 0,5, а некоторые могут варьироваться на 5. В случае с форексом данные значения существенно отличаются от упомянутых примеров. Такое разнообразие активов заставляет некоторых программистов разрабатывать советники специально для конкретных активов. Причина ясна: торговый сервер не принимает произвольные значения; нам необходимо придерживаться правил, установленных сервером. Тот же принцип применим и к системе репликации/моделирования. Мы не можем позволить советнику исполнять ордеры со случайными значениями.

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


Начнем с первого класса: Класс C_Terminal

Хотя зачастую можно написать весь код без специального руководства, данный подход не рекомендуется для проектов, которые потенциально могут стать очень большими и сложными. Мы до сих пор не знаем на сто процентов как будет развиваться проект, но важно всегда начинать, фокусируясь на лучших практиках программирования. Это позволяет избежать огромного объема беспорядочного кода без практического моделирования. Поэтому важно с самого начала мыслить масштабно, даже если проект окажется не таким грандиозным и сложным. Внедрение передового опыта делает нас более организованными даже в небольших проектах и ​​учит нас следовать стабильной методологии. Хорошо, давайте начнем с разработки первого класса. Для этого мы создадим новый заголовочный файл под названием C_Terminal.mqh. Хорошей практикой является присвоение файлу того же имени, что и у класса; это облегчит поиск, когда нам понадобится работать с ним. Код начинается следующим образом:

class C_Terminal
{

       protected:
   private  :
   public   :
};

В статье "Как построить советник, работающий автоматически (Часть 05): Ручные триггеры (II)", мы рассмотрели некоторые понятия о классах и зарезервированные слова, представленные здесь. Это стоит посмотреть, если вы еще незнакомы с серией о создании автоматизированного советника, поскольку она содержит множество элементов, которые мы здесь будем использовать. Хотя этот код устарел, в этом проекте мы рассмотрим новые формы взаимодействия, чтобы решить определенные вопросы и сделать систему еще более устойчивой, надежной и эффективной. Первое, что действительно появится в коде класса после начала написания кода, — это структура, которая будет подробно описана ниже:

class C_Terminal
{
   protected:
//+------------------------------------------------------------------+
      struct st_Terminal
      {
         long    ID;
         string  szSymbol;
         int     Width,
                 Height,
                 nDigits;
         double  PointPerTick,
                 ValuePerPoint,
                 VolumeMinimal,
                 AdjustToTrade;
      };
//+------------------------------------------------------------------+

Следует отметить, что эта структура объявляется внутри приватной части кода, что имеет решающее значение для того, что мы собираемся делать. Интересно отметить, что здесь мы не объявляем никаких переменных. Фактически, любая глобальная переменная внутри класса должна быть объявлена ​​в приватной части кода, что обеспечивает высочайший уровень безопасности и инкапсуляции информации. Мы будем возвращаться к этой теме на протяжении всей реализации для более глубокого понимания. Для лучшей практики программирования важно избегать того, чтобы внутренние переменные класса были доступны из других частей кода, которые не принадлежат классу.

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

   private :
      st_Terminal m_Infos;

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

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

#define macroGetDate(A) (A - (A % 86400))

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

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

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

Создав заголовочный файл Macros.mqh, в котором будут определены все глобальные макросы, мы приступим к включению данного файла в заголовок C_Terminal.mqh. Включение будет происходить таким образом:

#include "Macros.mqh"

Обратите внимание, что имя заголовочного файла заключено в двойные кавычки. Почему это указано так, а не между знаками меньше и больше (< >)? Есть ли для этого какая-то особая причина!? Да, есть. Используя двойные кавычки, мы сообщаем компилятору, что путь к файлу заголовка должен начинаться в каталоге, где находится текущий файл заголовка, в данном случае C_Terminal.mqh. Поскольку конкретный путь не указан, компилятор будет искать файл Macros.mqh в том же каталоге, где находится файл C_Terminal.mqh. Таким образом, если структура каталогов проекта была изменена, но мы сохраним файл Macros.mqh в том же каталоге, что и файл C_Terminal.mqh, то сообщать компилятору новый путь не потребуется.

Используя имя, заключенное в знаки меньше и больше (< >), мы указываем компилятору на то, чтобы начать поиск файла в предопределенном каталоге в системе сборки. Для MQL5 данным каталогом является INCLUDE. Поэтому любой путь к файлу Macros.mqh необходимо указывать из данного каталога INCLUDE, расположенного в папке MQL5. Если структура каталогов проекта будет изменена, тогда будет необходимо переопределить все пути, чтобы компилятор мог найти файлы заголовков. Хотя это может показаться незначительной деталью, выбор того или иного метода может играть важную роль.

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

void CurrentSymbol(void)
   {
      MqlDateTime mdt1;
      string sz0, sz1;
      datetime dt = macroGetDate(TimeCurrent(mdt1));
      enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
                
      sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
      for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
      switch (eTS)
      {
         case DOL:
         case WDO: sz1 = "FGHJKMNQUVXZ"; break;
         case IND:
         case WIN: sz1 = "GJMQVZ";       break;
         default : return;
      }
      for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
      if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;
   }

Представленный выше код, хотя и может показаться сложным и запутанным, имеет достаточно специфическую функцию: генерировать имя актива, чтобы его можно было использовать в системе кросс-ордеров. Чтобы понять, как это сделать, важно тщательно проанализировать процесс. В настоящее время основное внимание уделяется созданию имен для операций с фьючерсным индексом и фьючерсом на доллар в соответствии с номенклатурой B3 (Бразильской фондовой биржи). Понимая логику создания этих имен, можно адаптировать код для генерации имен любого будущего контракта, обеспечивая работу этих контрактов через систему кросс-ордеров, как обсуждалось ранее в статье "Разработка торгового советника с нуля (Часть 11): Система кросс-ордеров". Однако цель здесь состоит в том, чтобы расширить данную функциональность, чтобы советник адаптировался к различным условиям, сценариям, активам или рынкам. Это потребует от советника возможности определять, с каким видом актива он будет иметь дело, что может привести к необходимости включения в код большего количества различных видов активов. Чтобы объяснить это лучше, давайте разобьем его на более мелкие части.

MqlDateTime mdt1;
string sz0, sz1;
datetime dt = macroGetDate(TimeCurrent(mdt1));

Три упомянутые строки — это переменные, которые будут использоваться в коде. Тут основная проблема может быть связана со способом инициализации этих переменных. Функция TimeCurrent используется для инициализации двух разных переменных за один шаг. Первая переменная,mdt1, представляет собой структуру типа MqlDateTime, которая хранит детализированную информацию о дате и времени в самой переменной `mdt1`, а TimeCurrent также возвращает значение, хранящееся в `mdt1`. Вторая переменная, `dt`, использует макрос для выделения значения даты и его сохранения, что позволяет полностью инициализировать две переменные в одной строке кода.

enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;

Действительно, данная строка может показаться необычной, поскольку мы одновременно создаем перечисление, объявляем переменную и присваиваем ей начальное значение. Понимание этой линии имеет ключевое значение для понимания другой части процедуры. Обратите внимание на следующие моменты: В MQL5 перечисление невозможно создать без имени, поэтому указываем имя в самом начале. Внутри перечисления у нас есть элементы, которые по умолчанию начинаются с нулевого значения. Это можно изменить, но займемся этим позже, а пока запомните: по умолчанию перечисление начинается с нуля. Таким образом, значение WIN равно нулю, IND равно единице, а WDO равно двум и так далее. По причине, которая будет объяснена позже, последним элементом должно быть OTHER, независимо от количества элементов, которые мы хотим включить; но последний всегда должен быть OTHER. После определения перечисления мы объявляем переменную, которая будет использовать данные из этого перечисления, начиная со значения данной переменной как значения последнего элемента, т. е. OTHER.

Важная заметка: Посмотрите на объявление перечисления. Не кажется ли вам оно знакомым? Обратите внимание, что имена также объявляются заглавными буквами, что очень важно. Происходит следующее: Если мы хотим добавить дополнительные активы для использования в будущем контракте, мы должны сделать это, добавив первые три символа названия контракта перед элементом OTHER, чтобы процедура могла правильно сгенерировать имя текущего контракта. Например, если мы хотим добавить контракт BOI (заработанный), мы должны вставить значение BGI в перечисление, и это первый шаг. Есть еще один шаг, который обсудим позже, но для закрепления: если мы хотим добавить фьючерсный контракт на кукурузу, вы должны добавить значение CCM и т. д., всегда перед значением OTHER. В противном случае перечисление не будет работать как ожидается.

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

   sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3);
   for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS);
   switch (eTS)
   {
      case DOL:
      case WDO: sz1 = "FGHJKMNQUVXZ"; break;
      case IND:
      case WIN: sz1 = "GJMQVZ";       break;
      default : return;
   }

Первое выполняемое действие — сохранение имени актива в частной глобальной переменной класса. Чтобы упростить процесс, мы используем функцию StringSubstr для захвата и сохранения в переменной sz0 первых трех букв имени графического актива, в котором выполняется код класса. Это самый простой этап. Давайте теперь сделаем что-то очень необычное, но возможное: воспользуемся перечислением, чтобы определить, какое правило именования будет применено к контракту. Для этого мы используем цикл for. Выражение, используемое в данном цикле, может показаться довольно странным, однако то, что мы делаем - проходим по перечислению в поисках имени контракта, изначально определенного в перечислении, как я объяснял выше. Поскольку перечисление по умолчанию начинается со значения ноль, наша локальная переменная цикла также начнется с нуля. Независимо от того, какой элемент является первым, цикл начнется с него и продолжится до тех пор, пока не будет найден элемент OTHER или пока переменная eTS не будет отличаться от OTHER. На каждой итерации мы будем увеличивать позицию внутри перечисления. Теперь самое интересное: с помощью MQL5 мы используем функцию EnumToString, чтобы на каждой итерации цикла конвертировать значение перечисления в строку и сравнить его со значением, присутствующим в переменной sz0. Когда эти значения совпадают, позиция сохраняется в переменной eTS, в результате чего она уже начинает отличаться от OTHER. Данная процедура очень интересна и показывает, что перечисление в MQL5 не следует рассматривать так же, как в других языках программирования. Здесь можно рассматривать перечисление как массив строк, предлагающий гораздо больше функциональности и практичности, чем в других языках.

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

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

Теперь мы изучим еще один цикл процедуры, это этап, на котором, как обычно говорят, "всё усложняется". Сложность данного цикла может сбить с толку многих программистов, затрудняя понимание его функциональности. Поэтому важно уделить еще больше внимания следующему объяснению. Давайте тогда посмотрим на код этого цикла во фрагменте ниже:

for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0))
	if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;

Хотя на первый взгляд код может показаться запутанным и сложным, на самом деле он простой. Мы его сократили таким образом, чтобы сделать написание кода более эффективным. Чтобы сделать всё проще и избежать ненужной сложности мы используем командуIF, хотя это и не является строго необходимым. Теоретически всю команду можно включить в цикл FOR, но это немного усложнило бы объяснение. Поэтому мы используем в этом цикле `IF` для проверки соответствия имени сгенерированного контракта и имени, присутствующего на торговом сервере, чтобы определить, какой из будущих контрактов является наиболее актуальным. Чтобы понять данный процесс, важно знать правило именования, используемое для создания имени контракта. В качестве примера мы просмотрим, что происходит с фьючерсным мини-долларовым контрактом, торгуемым на B3 (Бразильской фондовой бирже), который следует определенному правилу номенклатуры:

  • Первые 3 символа названия контракта будут WDO. Независимо от даты истечения срока действия или того, является ли это историческим контрактом или нет;
  • Далее у нас идет символ, указывающий месяц истечения срока действия;
  • После этого символа у нас есть двузначное значение, указывающее год истечения срока действия.

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

Сначала мы инициализируем в цикле три локальные переменные, которые будут работать как необходимые нам единицы учета. Цикл выполняет свою первую итерацию, которая, что особенно интересно, происходит не внутри тела цикла, а в команде `if`. Однако тот же код, что и в команде `if`, можно поместить между двоеточиями (;) в цикле `for`, и цикл будет работать идентично. Давайте разберемся, что происходит в этом взаимодействии. Сначала мы создаем название контракта, следуя конкретным правилам его формирования. С помощью функции `StringFormat` мы получаем необходимое имя, которое будет сохранено как имя символа, к которому мы сможем получить доступ позже. Когда у нас уже есть имя контракта, мы делаем запрос к торговому серверу, чтобы узнать одно из свойств актива: время срока действия контракта, используя перечислениеSYMBOL_EXPIRATION_TIME. Функция `SymbolInfoInteger` вернет значение, но нас интересует только дата. Чтобы отфильтровать это, мы используем наш макрос, позволяющий нам сравнивать дату истечения срока с текущей датой. Если возвращаемое значение представляет собой будущую дату, цикл завершится, так как мы определим в переменной самый последний контракт. Однако на начальном этапе это вряд ли произойдет, поскольку год начинается c 2000 года, то есть уже в прошлом, поэтому потребуется новая итерация. Прежде чем повторять весь описанный процесс, нам необходимо увеличить позицию для создания нового имени контракта. Здесь нужно проявить осторожность, так как это увеличение должно быть сделано первым в коде истечения срока действия. Только если ни один из кодов истечения срока действия не будет удовлетворительным в этом году, то мы увеличим год. Данное действие выполним в три этапа, но в коде мы используем два тернарных оператора для выполнения этого приращения.

Прежде чем цикл выполнит новую итерацию и даже до выполнения тернарных операторов, мы увеличим значение, указывающее символ месяца истечения срока действия. После этого увеличения мы проверяем, находится ли значение в допустимых пределах с помощью первого тернарного оператора. Таким образом, индекс всегда будет указывать одно из значений, действительных на месяц истечения срока действия. Следующий шаг — мы проверим месяц истечения срока действия вторым тернарным оператором. Если индекс месяца истечения срока действия равен нулю, то все месяцы были проверены. Затем мы увеличим текущий год для новой попытки найти действительный контракт, и эта проверка произойдёт снова в команде `if`. Сам процесс повторяется до тех пор, пока не будет найден действительный контракт, демонстрируя, как система ищет имя текущего контракта. Нет, это не магия, а сочетание математики с программированием.

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

Давайте теперь проанализируем следующий код, который ссылается на конструктор нашего класса:

C_Terminal()
{
   m_Infos.ID = ChartID();
   CurrentSymbol();
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
   m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
   m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
   m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
   m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
   m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
   m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
   m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
}

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

Следующий код, который мы рассмотрим, — это деструктор класса, показанный ниже:

~C_Terminal()
{
   ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true);
   ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true);
   ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
}

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


   private :
      st_Terminal m_Infos;
      struct mem
      {
         long    Show_Descr,
                 Show_Date;
      }m_Mem;
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+          
      C_Terminal()
      {
         m_Infos.ID = ChartID();
         CurrentSymbol();
         m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR);
         m_Mem.Show_Date  = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false);
         m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS);
         m_Infos.Width   = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);                                
	 m_Infos.Height  = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         m_Infos.PointPerTick  = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE);
         m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE);
         m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP);
         m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
      }
//+------------------------------------------------------------------+
      ~C_Terminal()
      {
         ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date);
         ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr);
         ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false);
      }

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

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

//+------------------------------------------------------------------+
inline const st_Terminal GetInfoTerminal(void) const 
{

   return m_Infos;
}
//+------------------------------------------------------------------+
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{

   switch (id)
   {
      case CHARTEVENT_CHART_CHANGE:
         m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
         m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
         break;
   }
}
//+------------------------------------------------------------------+

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


Заключение

Из того материала, который мы рассмотрели в этой статье, мы уже сформировали наш базовый класс C_Terminal. Однако есть еще процедура, которую мы должны обсудить. Об этом будет рассказываться в следующей статье, где мы создадим класс C_Mouse. То, что мы рассмотрели здесь, уже позволяет нам использовать класс для создания чего-то полезного. Хотя мы не включили какой-нибудь сопутствующий код, это оправдано тем, что наша работа только начинается. Любой представленный сейчас код не будет иметь практического применения. В следующей статье нашей целью будет создание чего-то действительно полезного, позволяющего начать работу с графиком. Мы разработаем индикаторы и другие инструменты поддержки операций на счетах DEMO и REAL, и ​​даже в системе репликации/моделирования. Увидимся в следующей статье!

Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/11328

Разработка системы репликации (Часть 27): Проект советника — класс C_Mouse (I) Разработка системы репликации (Часть 27): Проект советника — класс C_Mouse (I)
В этой статье мы воплотим в жизнь класс C_Mouse. Он обеспечивает возможности программирования на самом высоком уровне. Однако разговоры о высокоуровневых или низкоуровневых языках программирования не связаны с включением в код нецензурных слов или жаргона. Всё наоборот. Когда мы говорим о высокоуровневом или низкоуровневом программировании, мы имеем в виду, насколько легко или сложно понять код другим программистам.
Альтернативные показатели риска и доходности в MQL5 Альтернативные показатели риска и доходности в MQL5
В этой статье мы представим реализацию нескольких показателей доходности и риска, рассматриваемых как альтернативы коэффициенту Шарпа, и исследуем гипотетические кривые капитала для анализа их характеристик.
Теория категорий в MQL5 (Часть 23): Другой взгляд на двойную экспоненциальную скользящую среднюю Теория категорий в MQL5 (Часть 23): Другой взгляд на двойную экспоненциальную скользящую среднюю
В этой статье мы продолжаем рассматривать популярные торговые индикаторы под новым углом. Мы собираемся обрабатывать горизонтальную композицию естественных преобразований. Лучшим индикатором для этого является двойная экспоненциальная скользящая средняя (Double Exponential Moving Average, DEMA).
Разработка системы репликации - Моделирование рынка (Часть 25): Подготовка к следующему этапу Разработка системы репликации - Моделирование рынка (Часть 25): Подготовка к следующему этапу
В этой статье мы завершаем первый этап разработки системы репликации и моделирования. Дорогой читатель, этим достижением я подтверждаю, что система достигла продвинутого уровня, открывая путь для внедрения новой функциональности. Цель состоит в том, чтобы обогатить систему еще больше, превратив ее в мощный инструмент для исследований и развития анализа рынка.