English Deutsch 日本語
preview
Парадигмы программирования (Часть 2): Объектно-ориентированный подход к разработке советника на основе ценовой динамики

Парадигмы программирования (Часть 2): Объектно-ориентированный подход к разработке советника на основе ценовой динамики

MetaTrader 5Примеры | 16 июля 2024, 16:43
69 2
Kelvin Muturi Muigua
Kelvin Muturi Muigua

Введение

В первой статье мы с вами познакомились с тем, как реализовать процедурное программирование на MQL5. Также мы коснулись темы функционального программирования. Мы познакомились с основами процедурного программирования и создали базовый советник Price Action с использованием индикатора экспоненциальной скользящей средней (EMA) и свечных данных.

В этот раз мы перейдем к рассмотрению парадигмы объектно-ориентированного программирования. Затем применим полученные знания для преобразования процедурного кода ранее разработанного советника из первой статьи в объектно-ориентированный код. Это позволит нам лучше понять основные различия между двумя парадигмами.

Хочу обратить ваше внимание на то, что цель статьи не в том, чтобы продемонстрировать работу стратегии ценового действия. Я хочу показать, как функционируют различные парадигмы программирования и как их реализовать в MQL5. Простой советник Price Action, который мы разрабатываем, всего лишь демонстрирует, как применять знания на практике.


Понимание объектно-ориентированного программирования

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

При начале работы с объектно-ориентированным программированием, особенно при переходе с процедурного, часто возникают вопросы. Начнем с таких вопросов, чтобы дальше усваивать материал было проще.


Что такое класс в объектно-ориентированном программировании?


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

В качестве примера рассмотрим телефон.

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

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

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

  • Совершать и принимать телефонные звонки.
  • Отправлять и получать SMS.
  • Отправлять и получать данные через Интернет.
  • Делать фотографии и записывать видео.

В объектно-ориентированном программировании такие задачи называются методами. Методы аналогичны обычным функциям, но при создании в классе они называются методами.

Затем вы решаете, что каждый телефон должен иметь определенные свойства и характеристики. Для этого вы составляете такой список:

  • Номер модели.
  • Цвет.
  • Тип ввода.
  • Тип экрана.
  • Размер экрана.

В ООП свойства и характеристики, описанные в модели (классе), называются атрибутами или переменными класса. Атрибуты объявляются в классе как переменные.


Что такое объект в объектно-ориентированном программировании?


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

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

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

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

Давайте теперь посмотрим на пример этого телефона в коде. Следуя этим шагам, вы можете создать файл класса в IDE MetaEditor.


Шаг 1: Откройте MetaEditor и запустите Мастера, нажав кнопку "Создать".

Создаем новый include-файл в Мастере MetaEditor


Шаг 2: Выберите пункт Новый класс и нажмите Далее.

Новый файл класса в Мастере MQL


Шаг 3: В окне создания класса, в поле Название класса: укажите PhoneClass, а в поле Include-файл: укажите Experts\OOP_Article\PhoneClass.mqh, чтобы сохранить файл класса в той же папке, где и исходный код нашего советника. Поле Базовый класс: оставляем пустым. Нажимаем "Готово", и файл нового класса на MQL5 будет создан.

Детали нового класса в Мастере MQL


Теперь у нас есть заготовка файла класса MQL5. Я добавил комментарии, чтобы объяснить различные части класса. На самом деле новый класс на MQL5 создать достаточно просто с автоматическим Мастером MQL в редакторе MetaEditor IDE. Посмотрите на синтаксис в коде ниже — он содержит отправную точку правильно структурированного файла класса MQL5.

class PhoneClass // имя класса
  {
private: // модификатор доступа

public:  // модификатор доступа
                     PhoneClass(); // объявление конструктора
                    ~PhoneClass(); // объявление деструктора
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
PhoneClass::PhoneClass() // определение конструктора
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
PhoneClass::~PhoneClass() // определение деструктора
  {
  }
//+------------------------------------------------------------------+


Не обращайте внимания на код в #properties, поскольку он пока не имеет отношения к рассматриваемой теме. Интересующий нас синтаксис начинается со строки, где находится синтаксис начала класса :class PhoneClass {чуть ниже строки #property version "1.00".

//+------------------------------------------------------------------+
//|                                                   PhoneClass.mqh |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//---------------------------------------
// игнорируем фрагмент кода выше
//---------------------------------------


Давайте посмотрим на файл класса, который мы только что сгенерировали.

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

Под модификаторами доступа private добавим атрибуты класса (свойства и характеристики телефона) из проекта нашего телефона. Добавим эти свойства как глобальные переменные.

private: // модификатор доступа
   // атрибуты класса
   int               modelNumber;
   string            phoneColor;
   string            inputType;
   string            screenType;
   int               screenSize;

На строке под модификатором доступа public: идут объявления конструктора и деструктора. Эти два метода аналогичны стандартным функциям советников OnInit() и OnDeInit(). Конструктор класса выполняет операции, аналогичные OnInit(), а деструктор — аналогичные OnDeinit().

public: // модификатор доступа
                     PhoneClass(); // объявление конструктора
                    ~PhoneClass(); // объявление деструктора


Что такое конструкторы и деструкторы в объектно-ориентированном программировании?


Конструкторы

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

Основные характеристики конструкторов:

  • Имя совпадает с именем и класса — конструктор имеет то же имя, что и класс. Такое правило именования позволяет языку программирования идентифицировать и связывать конструктор с классом. В MQL5 все конструкторы по умолчанию имеют тип void, то есть они не возвращают значение.
  • Инициализация — конструкторы отвечают за инициализацию атрибутов объекта значениями по умолчанию или указанными значениями. Это гарантирует, что объект начинается с четко определенного состояния. Я покажу, как это работает в нашем классе PhoneClass.
  • Автоматическое исполнение — конструктор автоматически вызывается или выполняется при создании объекта. Это происходит в момент создания экземпляра объекта.
  • Дополнительные параметры — конструкторы могут принимать дополнительные параметры, что позволяет настраивать их во время создания объекта. Эти параметры указывают значения, которые конструктор использует для установки начального состояния объекта.

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

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

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

   //PhoneClass();
   // объявление и определение конструктора
   PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen)
     {      
      modelNumber = modelNo;
      phoneColor  = colorOfPhone;
      inputType   = typeOfInput;
      screenType  = typeOfScreen;
      screenSize  = sizeOfScreen;
     }

Сохраним и скомпилируем файл класса. После компиляции файла класса вы увидите ошибку в строке 40 в столбце 13. Ошибка: PhoneClass — функция-член, уже определенная с другими параметрами.

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

Ошибка компиляции PhoneClass

Мы объявили и определили наш новый параметрический конструктор в одном блоке кода, а дальше по нашему коду в строке 40 идет еще один сегмент кода, который также определяет конструктор. Необходимо закомментировать строки с 40 по 42. После этого файл класса скомпилируется успешно, без ошибок и предупреждений. (Обратите внимание, что в вашем коде этот сегмент может находиться на других строках!)

/*PhoneClass::PhoneClass() // определение метода конструктора
  {
  }*/


Деструкторы

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

Основные характеристики деструкторов:

  • Имя совпадает с именем и класса — деструктор имеет то же имя, что и класс, но перед ним еще добавляется знак тильды (~). Такое правило именования позволяет языку программирования идентифицировать и связывать деструктор с классом.
  • Сбор мусора — очистка любых ресурсов, выделенных объектом, таких как память, строки, динамические массивы, автоматические объекты или сетевые подключения.
  • Автоматическое исполнение — деструктор автоматически вызывается или выполняется при завершении работы объекта.
  • Отсутствие параметров — деструкторы не имеют параметров и по умолчанию имеют тип void, что означает, что они не возвращают значения.

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

PhoneClass::~PhoneClass() // определение деструктора
  {
   Print("-------------------------------------------------------------------------------------");
   PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber);
  }

Как видите, при объявлении или определении методов конструктора и деструктора класса, мы не задаем им возвращаемый тип (void). Указывать тип возвращаемого значения не обязательно, поскольку существует простое правило: все конструкторы и деструкторы в MQL5 имеют тип void, и компилятор это знает.

Чтобы было проще понять, как работают конструкторы и деструкторы, рассмотрим краткий пример: Представьте себе поход, в котором вы и ваш друг распределяете между собой определенные роли. В роли «конструктора» выступает ваш друг, ответственный за установку палатки и организацию всего по прибытии. А вы занимаетесь упаковкой и уборкой по окончании поездки, то есть играете роль деструктора. В объектно-ориентированном программировании конструкторы инициализируют объекты, а деструкторы очищают ресурсы по окончании срока службы объекта.

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

// методы класса
   bool              MakePhoneCall(int phoneNumber);
   void              ReceivePhoneCall();
   bool              SendSms(int phoneNumber, string message);
   void              ReceiveSms();
   bool              SendInternetData();
   void              ReceiveInternetData();
   void              UseCamera();
   void virtual      PrintPhoneSpecs();


Что такое виртуальные методы в MQL5?

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

Мы увидим, как переопределить метод PrintPhoneSpecs(), позже в статье, при рассмотрении объектно-ориентированного наследования.

Чтобы осуществить определение метода для PrintPhoneSpecs(), разместим этот код под определением метода деструктора внизу нашего файла класса.

void PhoneClass::PrintPhoneSpecs() // определение метода
  {
   Print("___________________________________________________________");
   PrintFormat("Model: %i Phone Specs", modelNumber);
   Print("---------------------");
   PrintFormat
      (
         "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n",
         modelNumber, phoneColor, inputType, screenType, screenSize
      );  
  }

Существует два способа определения метода класса. 

  1. Внутри тела класса — объявить и определить метод можно за один шаг внутри тела класса, как мы делали ранее с параметрическим конструктором. Синтаксис объявления и определения метода внутри тела класса идентичен синтаксису обычной функции.
  2. Вне тела класса — сначала объявить метод внутри тела класса, а затем определить его вне тела класса, как мы сделали с деструктором и PrintPhoneSpecs(). Чтобы определить метод MQL5 вне тела класса, нужно сначала указать тип возвращаемого значения метода, затем имя класса, оператор области видимости (::), имя метода, а затем список параметров в скобках. Далее тело метода заключается в фигурные скобки {}.а Такое разделение объявления и определения является предпочтительным вариантом, поскольку позволяет четко организовать структуру класса и связанных с ним методов.

Что такое оператор области видимости (::) в объектно-ориентированном программировании?


Оператор :: — оператор разрешения области видимости, используется в C++ и MQL5 для указания контекста, к которому принадлежит функция или метод. Он определяет или ссылается на функции или методы, которые являются членами класса, и позволяют указать, что они являются членами этого конкретного класса.

Давайте рассмотрим более подробно на примере определения метода PrintPhoneSpecs():

void PhoneClass::PrintPhoneSpecs() // определение метода
  {
   // тело метода
  }

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


В нашем классе также объявлены следующие методы, которые также необходимо определить:

  1. MakePhoneCall(int phoneNumber);
  2. ReceivePhoneCall();
  3. SendSms(int phoneNumber, string message);
  4. ReceiveSms();
  5. SendInternetData();
  6. ReceiveInternetData();
  7. UseCamera();

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

class PhoneClass // имя класса
  {
private: // модификатор доступа
   // атрибуты класса
   int               modelNumber;
   string            phoneColor;
   string            inputType;
   string            screenType;
   int               screenSize;

public: // модификатор доступа
   //PhoneClass();
   // объявление и определение конструктора
   PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen)
     {      
      modelNumber = modelNo;
      phoneColor  = colorOfPhone;
      inputType   = typeOfInput;
      screenType  = typeOfScreen;
      screenSize  = sizeOfScreen;
     }
     
   ~PhoneClass(); // объявление деструктора
   
   // методы класса
   bool              MakePhoneCall(int phoneNumber);
   void              ReceivePhoneCall();
   bool              SendSms(int phoneNumber, string message);
   void              ReceiveSms();
   bool              SendInternetData();
   void              ReceiveInternetData();
   void              UseCamera();
   void virtual      PrintPhoneSpecs();
  };

/*PhoneClass::PhoneClass() // определение метода конструктора
  {
  }*/

PhoneClass::~PhoneClass() // определение деструктора
  {
   Print("-------------------------------------------------------------------------------------");
   PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber);
  }

bool PhoneClass::MakePhoneCall(int phoneNumber) // определение метода
  {
      bool callMade = true;
      Print("Making phone call...");
      return(callMade);
  }

void PhoneClass::ReceivePhoneCall(void) { // определение метода
      Print("Receiving phone call...");
   }

bool PhoneClass::SendSms(int phoneNumber, string message) // определение метода
  {
      bool smsSent = true;
      Print("Sending SMS...");
      return(smsSent);
  }

void PhoneClass::ReceiveSms(void) { // определение метода
      Print("Receiving SMS...");
   }

bool PhoneClass::SendInternetData(void) // определение метода
  {
      bool dataSent = true;
      Print("Sending internet data...");
      return(dataSent);
  }
  
void PhoneClass::ReceiveInternetData(void) { // определение метода
      Print("Receiving internet data...");
   }

void PhoneClass::UseCamera(void) { // определение метода
      Print("Using camera...");
   }

void PhoneClass::PrintPhoneSpecs() // определение метода
  {
   Print("___________________________________________________________");
   PrintFormat("Model: %i Phone Specs", modelNumber);
   Print("---------------------");
   PrintFormat
      (
         "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n",
         modelNumber, phoneColor, inputType, screenType, screenSize
      );  
  }

Вы можете найти полный код PhoneClass.mqh в приложениях к статье.

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

Файлы классов сохраняются с расширением .mqh и называются включаемыми файлами. Создадим новый советник в той же папке, в которой хранится наш файл PhoneClass.mqh. Мы сохранили agqk PhoneClass.mqh в папке: Experts\OOP_Article\. Используем Мастер MQL в MetaEditor, чтобы создать новый советник (шаблон), и сохраним его в папке Experts\OOP_Article\. Назовем новый советник PhoneObject.mq5.

Добавим следующий код под фрагментов #property version   "1.00" в коде советника:

// Подключаем файл PhoneClass, чтобы код PhoneClass стал доступен в этом советнике
#include "PhoneClass.mqh"

int OnInit()
  {
//---
   // Создаем экземпляры или объекты PhoneClass с определенными параметрами,
   // указанными в конструкторе 'PhoneClass'
   PhoneClass myPhoneObject1(101, "Black", "Keyboard", "Non-touch LCD", 4);
   PhoneClass myPhoneObject2(102, "SkyBlue", "Touchscreen", "Touch AMOLED", 6);

   // Вызов метода PrintPhoneSpecs для вывода спецификаций
   myPhoneObject1.PrintPhoneSpecs();
   myPhoneObject2.PrintPhoneSpecs();
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason){}
void OnTick(){}

Вы можете найти полный код файла PhoneObject.mq5 в приложениях к статье.

Давайте разберемся, что же происходит в коде советника PhoneObject.mq5:

Добавляем файл класса с помощью оператора Include:

Сначала мы добавляем в советник код PhoneClass с помощью директивы #include "PhoneClass.mqh", чтобы он стал доступен для использования в нашем только что созданном советнике (PhoneObject.mq5). Мы включили этот класс в глобальную область, и он будет доступен во всех разделах советника.


Создаем объект PhoneClass:

Внутри функции советника OnInit(), мы создали два экземпляра или объекта PhoneClass. Чтобы создать эти объекты, мы начали с имени класса, за которым добавили описательные имена экземпляров телефона (myPhoneObject1 и myPhoneObject2). Затем мы использовали круглые скобки, чтобы указать значения технических характеристик телефона, например номер модели, цвет, тип ввода, тип и размер экрана, как указано в параметрах конструктора PhoneClass.


Вызов метода класса:

Строки myPhoneObject1.PrintPhoneSpecs() и myPhoneObject2.PrintPhoneSpecs() вызывают метод PrintPhoneSpecs() объекта PhoneClass, чтобы вывести на экран характеристики телефона.


Вывод на экран характеристик телефона:

Запускаем советник на графике символов в торговом терминале МТ5, чтобы выполнить PhoneObjectEA, идем в окно Инструменты и выбираем вкладку Эксперты, чтобы проверить выведенные характеристики телефона.

Выведенные данные также отображают текстовое сообщение от деструктора PhoneClass (~PhoneClass()). Как видите, каждый объект-телефон создает уникальный независимый деструктор и вызывает его при завершении объекта.

Журнал экспертов PhoneObjectEA


Что такое наследование в объектно-ориентированном программировании?


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

Проще говоря, наследование похоже на генеалогическое древо. Базовый класс можно представить как родителя. Этот класс имеет определенные особенности (свойства и методы). Тогда подкласс — это ребенок. Подкласс автоматически наследует все черты (свойства и методы) базового класса, подобно тому, как ребенок наследует черты от своего родителя.

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

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

  • Базовый класс — родительский класс, суперкласс, корневой класс, базовый класс, мастер-класс.
  • Подкласс — дочерний класс, производный класс, класс-потомок, класс-наследник.

Ниже показано, как можно реализовать наследование в контексте предоставленного кода PhoneClass:

  • PhoneClass является базовым классом (проектом), определяющим основные строительные блоки функциональности телефона.
  • Создадим еще один класс для реализации модели телефона более продвинутого класса (смартфона), которую обсуждали ранее в примере с производством телефонов.
  • Назовем класс SmartPhoneClass. Наследуем все свойства и методы от класса PhoneClass, а также добавим новые характеристики, специфичные для смартфонов. Также переопределим существующий метод PrintPhoneSpecs() из класса PhoneClass, чтобы реализовать поведение смартфона.
Создадим новые пустой файл класса SmartPhoneClass.mqh так же, как мы создавали класс PhoneClass в Мастере MQL в MetaEditor. Добавим следующий код в тело SmartPhoneClass.mqh:

#include "PhoneClass.mqh" // Подключаем файл PhoneClass
class SmartPhoneClass : public PhoneClass
{
private:
    string operatingSystem;
    int numberOfCameras;

public:
    SmartPhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen, string os, int totalCameras)
        : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen)
    {
        operatingSystem = os;
        numberOfCameras = totalCameras;
    }

    void UseFacialRecognition()
    {
        Print("Using facial recognition feature...");
    }

    // При необходимости переопределим методы базового класса
    void PrintPhoneSpecs() override
    {
        Print("-----------------------------------------------------------");
        Print("Smartphone Specifications (including base phone specs):");
        Print("-----------------------------------------------------------");
        PrintFormat("Operating System: %s \nNumber of Cameras: %i", operatingSystem, numberOfCameras);
        PhoneClass::PrintPhoneSpecs(); // Call the base class method
        Print("-----------------------------------------------------------");
    }
};

Полный код доступен в файле SmartPhoneClass.mqh

В этом примере класс SmartPhoneClass наследуется от класса PhoneClass. При этом он добавляет новые свойства (operatingSystem и numberOfCameras), а также новый метод (UseFacialRecognition). Конструктор класса SmartPhoneClass также вызывает конструктор базового класса (PhoneClass) с помощью функции ': PhoneClass(...)'. Также можно заметить, что мы переопределили метод PrintPhoneSpecs() от базового класса. Мы включили спецификатор override в определение метода PrintPhoneSpecs() в классе SmartPhoneClass, чтобы компилятор знал, что мы намеренно переопределили метод базового класса.

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


Модификаторы доступа

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

  • Public — публичный доступ позволяет свойствам и методам класса быть доступными из мест вне класса. Можно использовать или менять любого публичного участника из любой части программы. Это самый открытый уровень доступа.
  • Private — частный доступ ограничивает видимость свойств и методов для доступа или изменения их областью самого класса. Члены, объявленные с модификатором private, не доступны напрямую извне класса. Это позволяет скрыть детали реализации и обеспечить целостность данных. Это называется инкапсуляцией.
  • Protected — защищенный доступ, золотая середина между публичным и частным. Члены, объявленные как защищенные, доступны внутри класса и в его подклассах (производных классах). Это обеспечивает определенный уровень контролируемого использования связанными классами, при этом ограничивая доступ извне.

Чтобы создать объект SmartPhoneClass, создадим новый советник и сохраним его как SmartPhoneObject.mq5. Добавим туда такой код:

// Подключаем файл PhoneClass, чтобы его код стал доступен в этом советнике
#include "SmartPhoneClass.mqh"

int OnInit()
  {
//---
   // Создаем экземпляры или объекты PhoneClass с определенными параметрами,
   // указанными в конструкторе базового класса PhoneClass
   PhoneClass myPhoneObject1(103, "Grey", "Touchscreen", "Touch LCD", 8);
   
   // as specified in the 'SmartPhoneClass' consturctor
   SmartPhoneClass mySmartPhoneObject1(104, "White", "Touchscreen", "Touch AMOLED", 6, "Android", 3);

   // Вызов метода PrintPhoneSpecs для вывода спецификаций
   myPhoneObject1.PrintPhoneSpecs(); // метод базового класса
   mySmartPhoneObject1.PrintPhoneSpecs(); // переопределенный метод класса-наследника (SmartPhoneClass)
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason){}
void OnTick(){}

Полный код файла SmartPhoneObject.mq5 доступен в приложениях к статье.

Сохраняем и компилируем исходный код SmartPhoneObject.mq5. После этого его можно было запустить на графике торгового инструмента в терминале MetaTrader 5 и посмотреть на выполнение объекта SmartPhoneClass. Переходим в окно Инструменты и выбираем вкладку Эксперты, чтобы проверить журнал данных советника.


Основные свойства объектно-ориентированного программирования

Шесть основных свойств ООП:

  1. Инкапсуляция — объединение данных и методов в один блок (класс), чтобы скрыть внутренние детали.
  2. Абстракция — упрощение сложных систем путем сосредоточения внимания на основных свойствах и поведении.
  3. Наследование — разрешение классу наследовать свойства и поведение от другого класса для повторного использования кода.
  4. Полиморфизм — разрешение рассматривать объекты разных классов как объекты общего базового класса для обеспечения гибкости.
  5. Классы и объекты — проекты-классы и экземпляры-объекты организуют код по модульному принципу.
  6. Передача сообщений — объекты общаются сообщениями и взаимодействуют.

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


Соглашение об именах классов в MQL5

В MQL5 к именам классов добавляется префикс С. Однако это не обязательно. Префикс С означает class (класс) и является общепринятой практикой. Он показывает, что конкретный идентификатор представляет класс.

Примеры имена классов в коде MQL5: CExpert, CIndicator, CStrategy и др. В целом соглашение об именах помогает отличать в данном случае классы от других типов идентификаторов, таких как функции или переменные.

Хотя использование префикса C является общепринятым и обычно рекомендуется для ясности, MQL5 не налагает каких-либо строгих правил относительно именования классов. Технически вы можете называть свои классы без префикса, но все же рекомендуется следовать установленным соглашениям, чтобы улучшить читаемость и удобство обслуживания кода.


Объектно-ориентированный подход к разработке советника на основе ценовой динамики

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

Краткий обзор особенностей торговой стратегии по ценовому действию (Price Action). Более подробное объяснение этой торговой стратегии представлено в первой статье.


Стратегия Price Action EMA


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

Правила входа:

  • ПОКУПКА Открываем позицию на покупку, когда последняя закрытая свеча является бычьей (открытие ниже закрытия), и ее минимальная и максимальная цены находятся выше экспоненциальной скользящей средней (EMA).
  • ПРОДАЖА Открываем позицию на продажу, когда последняя закрытая свеча является медвежьей (открытие выше закрытия), и ее минимальная и максимальная цены находятся ниже экспоненциальной скользящей средней (EMA).
  • Позиции открываются на каждом новом баре при соблюдении озвученных выше условий.

Правила выхода:

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

Обзор стратегии ценового действия EMA


Цель этой статьи — продемонстрировать, как превратить описанную выше стратегию в советник mql5, используя объектно-ориентированные принципы. Поэтому перейдем к написанию кода.

Создадим новый класс CEmaExpertAdvisor


Используя Мастера MQL5, создадим заготовку файла класса с именем EmaExpertAdvisor и сохраним его в папке Experts\OOP_Article\PriceActionEMA\. Внутри вновь созданного файла EmaExpertAdvisor.mqh создадим новый класс с именем CEmaExpertAdvisor. Мы будем использовать класс CEmaExpertAdvisor для инкапсуляции поведения советника и переменные для представления его состояния.

Добавляем следующий код свойств/членов класса и коды методов в файл CEmaExpertAdvisor:

//+------------------------------------------------------------------+
// Подключаем торговый класс из стандартной библиотеки
//--- 
#include <Trade\Trade.mqh>

class CEmaExpertAdvisor
  {

public:
   CTrade            myTrade;
   
public:
   // Конструктор
                     CEmaExpertAdvisor(
      long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe,
      int _emaPeriod, int _emaShift, bool _enableTrading,
      bool _enableAlerts, double _accountPercentageProfitTarget,
      double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl
   );

   // Деструктор
                    ~CEmaExpertAdvisor();
  };

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

#include <Trade\Trade.mqh>

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

//Создаем экземпляр/объект включенного класса CTrade
   CTrade myTrade;

Все вводимые пользователем глобальные переменные из процедурного кода станут частными глобальными переменными класса CEmaExpertAdvisor. Входные переменные советник необходимо будет инициализировать сразу после создания экземпляра класса. Для этого передадим их в параметрах конструктору. Это позволит инкапсулировать процесс инициализации внутри класса.

private:
   // Частные переменные/атрибуты (ранее процедурные глобальные переменные)
   //------------------------
   // Пользовательские входные переменные
   long              magicNumber;
   ENUM_TIMEFRAMES   tradingTimeframe;
   int               emaPeriod;
   int               emaShift;
   bool              enableTrading;
   bool              enableAlerts;
   double            accountPercentageProfitTarget;
   double            accountPercentageLossTarget;
   int               maxPositions;
   int               TP;
   int               SL;

Остальные глобальные переменные процедурного кода будут объявлены публичными (public) и определены как глобальные переменные в классе.

public:
   //--- Глобальные переменные советника
   // Переменные скользящей средней
   double            movingAverage[];
   int               emaHandle;
   bool              buyOk, sellOk;
   string            movingAverageTrend;

   // Строки для комментариев на графике
   string            commentString, accountCurrency, tradingStatus, accountStatus;

   // Переменные для управления капиталом
   double            startingCapital, accountPercentageProfit;

   // Переменные ордеров и позиций
   int               totalOpenBuyPositions, totalOpenSellPositions;
   double            buyPositionsProfit, sellPositionsProfit, buyPositionsVol, sellPositionsVol;

   datetime          closedCandleTime;// используется для  определения формирования свечи

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

// Объявления методов класса (ранее — отдельные процедурные функции)
   int               GetInit();
   void              GetDeinit();
   void              GetEma();
   void              GetPositionsData();   
   bool              TradingIsAllowed();
   void              TradeNow();
   void              ManageProfitAndLoss();
   void              PrintOnChart();
   bool              BuySellPosition(int positionType, string positionComment);
   bool              PositionFound(string symbol, int positionType, string positionComment);

Используем стиль кода из C++ и определим все методы класса под телом класса:

//+------------------------------------------------------------------+
//|   ОПРЕДЕЛЕНИЯ МЕТОДОВ                                                                |
//+------------------------------------------------------------------+
CEmaExpertAdvisor::CEmaExpertAdvisor(long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe,
                                   int _emaPeriod, int _emaShift, bool _enableTrading,
                                   bool _enableAlerts, double _accountPercentageProfitTarget,
                                   double _accountPercentageLossTarget,
                                   int _maxPositions, int _tp, int _sl)
  {
   magicNumber = _magicNumber;
   tradingTimeframe = _tradingTimeframe;
   emaPeriod = _emaPeriod;
   emaShift = _emaShift;
   enableTrading = _enableTrading;
   enableAlerts = _enableAlerts;
   accountPercentageProfitTarget = _accountPercentageProfitTarget;
   accountPercentageLossTarget = _accountPercentageLossTarget;
   maxPositions = _maxPositions;
   TP = _tp;
   SL = _sl;
  }
//+------------------------------------------------------------------+
CEmaExpertAdvisor::~CEmaExpertAdvisor() {}
//+------------------------------------------------------------------+
int CEmaExpertAdvisor::GetInit()
  {
    // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetDeinit()
  {
    // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetEma()
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::GetPositionsData()
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::TradingIsAllowed()
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::TradeNow()
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::ManageProfitAndLoss()
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
void CEmaExpertAdvisor::PrintOnChart()
  {
   // тело метода....
  } 
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment)
  {
   // тело метода....
  }
//+------------------------------------------------------------------+
bool CEmaExpertAdvisor::PositionFound(string symbol, int positionType, string positionComment)
  {
   // тело метода....
  }

Все определения методов будут идентичны синтаксису из процедурного кода за исключением методов ManageProfitAndLoss() и BuySellPosition(...), которые мы обновили для использования вновь созданного объекта myTrade из класса CTrade, ранее импортированного в код класса.

Ниже показан новый метод ManageProfitAndLoss():

void CEmaExpertAdvisor::ManageProfitAndLoss()
  {
//если процент прибыли или убытка счета достигнут, удалить все позиции
   double lossLevel = -accountPercentageLossTarget;
   if(
      (accountPercentageProfit >= accountPercentageProfitTarget || accountPercentageProfit <= lossLevel) ||
      ((totalOpenBuyPositions >= maxPositions || totalOpenSellPositions >= maxPositions) && accountPercentageProfit > 0)
   )
     {
      //удаляем все открытые позиции
      if(PositionsTotal() > 0)
        {
         //переменные для хранения значений свойств позиции
         ulong positionTicket;
         long positionMagic, positionType;
         string positionSymbol;
         int totalPositions = PositionsTotal();

         //сканируем все открытые позиции
         for(int x = totalPositions - 1; x >= 0; x--)
           {
            positionTicket = PositionGetTicket(x);//получим доступ к другим свойствам позиции по тикету
            positionMagic = PositionGetInteger(POSITION_MAGIC);
            positionSymbol = PositionGetString(POSITION_SYMBOL);
            positionType = PositionGetInteger(POSITION_TYPE);
            int positionDigits= (int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS);
            double positionVolume = PositionGetDouble(POSITION_VOLUME);
            ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

            if(positionMagic == magicNumber && positionSymbol == _Symbol) //close the position
              {
               // выводим детали позиции
               Print("*********************************************************************");
               PrintFormat(
                  "#%I64u %s  %s  %.2f  %s [%I64d]",
                  positionTicket, positionSymbol, EnumToString(positionType), positionVolume,
                  DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic
               );
               
               // выводим детали закрытия позиции
               PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
               // отправляем торговый запрос tradeRequest
               if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3)) //успешно, позиция закрыта
                 {
                  if(enableAlerts)
                    {
                     Alert(
                        _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION (#" +
                        IntegerToString(positionTicket) + "). Check the EA journal for more details."
                     );
                    }
                  PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
                  myTrade.PrintResult();
                 }
               else  // отправка tradeRequest не удалась
                 {
                  // вывод информации об операции
                  if(enableAlerts)
                    {
                     Alert(
                        _Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION (#" +
                        IntegerToString(positionTicket) + "). Check the EA journal for more details."
                     );
                    }
                  PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
                  PrintFormat("OrderSend error %d", GetLastError());// вывод кода ошибки
                 }
              }
           }
        }
     }
  }


Далее идет новый метод BuySellPosition(...):

bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment)
  {
   double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / 10000), 2);
   double tpPrice = 0.0, slPrice = 0.0, symbolPrice;
   
   if(positionType == POSITION_TYPE_BUY)
     {
      if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200)
        {
         volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), 2);
        }
      if(volumeLot < 0.01)
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
        }
      if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
        }
      
      volumeLot = NormalizeDouble(volumeLot, 2);
      symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      
      if(TP > 0)
        {
         tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits);
        }
      if(SL > 0)
        {
         slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits);
        }
      //if(myTrade.Buy(volumeLot, NULL, 0.0, 0.0, 0.0, positionComment)) // позиция открыта успешно
      if(myTrade.Buy(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) // позиция открыта успешно
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " Successfully openend BUY POSITION!");
           }
         myTrade.PrintResult();
         return(true);
        }
      else
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " ERROR opening a BUY POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK));
           }
         PrintFormat("ERROR: Opening a BUY POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code
         return(false);
        }
     }

   if(positionType == POSITION_TYPE_SELL)
     {
      if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200)
        {
         volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), 2);
        }
      if(volumeLot < 0.01)
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
        }
      if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
        {
         volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
        }
      volumeLot = NormalizeDouble(volumeLot, 2);
      symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(TP > 0)
        {
         tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits);
        }
      if(SL > 0)
        {
         slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits);
        }
      if(myTrade.Sell(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) // позиция открыта успешно
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " Successfully openend SELL POSITION!");
           }
           myTrade.PrintResult();
         return(true);
        }
      else
        {
         if(enableAlerts)
           {
            Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK));
           }
         PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code
         return(false);
        }
     }
   return(false);
  }

Не забудем реализовать все методы/функции с логикой исходного процедурного кода. Весь код класса CEmaExpertAdvisor доступен в include-файле EmaExpertAdvisor.mqh, приложенном к статье.


Создаем новый советник OOP_PriceActionEMA


Мы подготовили макет торговой стратегии (класс CEmaExpertAdvisor). Теперь пришло время применить все это на практике. Воплотим этот макет в жизнь и создадим реальный объект, который сможет совершать сделки.

Создадим новый советник с именем "OOP_PriceActionEMA.mq5" и сохраним его в папке Experts\OOP_Article\PriceActionEMA. Советник будет отвечать за реализацию нашей торговой стратегии.

Начнем с импорта включаемого файла EmaExpertAdvisor.mqh, в котором находится класс CEmaExpertAdvisor.

// Подключаем файл CEmaExpertAdvisor, чтобы его код стал доступен в этом советнике
#include "EmaExpertAdvisor.mqh"

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

//-- Входные параметры
input long magicNumber = 101;// Магический номер (или 0, чтобы отключить)

input group ""
input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;// торговый таймфрейм
input int emaPeriod = 15;//период скользящей средней
input int emaShift = 0;// сдвиг скользящей средней

input group ""
input bool enableTrading = true;// Включить торговлю
input bool enableAlerts = false;// Включить алерты

input group ""
input double accountPercentageProfitTarget = 6.0;// Целевая прибыль в % от счета
input double accountPercentageLossTarget = 10.0;// Макс убыток в % от счета

input group ""
input int maxPositions = 3;// макс кол-во позиций в одном направлении
input int TP = 5000;//TP (в пунктах, 0 отключает)
input int SL = 500;//SL (в пунктах, 0 отключает)

После этого создадим экземпляр класса CEmaExpertAdvisor. Здесь создается экземпляр (ea) класса CEmaExpertAdvisor, используя конструктор. Инициализируем пользовательскими входными переменными.

//Создаем экземпляр/объект включенного класса CEmaExpertAdvisor
//с пользовательскими данными, указанными в параметрах конструктора
CEmaExpertAdvisor ea(
      magicNumber, tradingTimeframe, emaPeriod, emaShift,
      enableTrading, enableAlerts, accountPercentageProfitTarget,
      accountPercentageLossTarget, maxPositions, TP, SL
   );

В функции OnInit вызываем метод GetInit экземпляра советника ea. Этот метод является частью класса CEmaExpertAdvisor, он отвечает за инициализацию советника. Если инициализация не удалась, возвращается INIT_FAILED. В противном случае возвращается INIT_SUCCEEDED.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(ea.GetInit() <= 0)
     {
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }

В функции OnDeinit вызываем метод GetDeinit экземпляра ea. Метод является частью класса CEmaExpertAdvisor, он отвечает за деинициализацию советника.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   ea.GetDeinit();
  }

В функции OnTick вызываем разные методы экземпляра ea, такие как GetEma, GetPositionsData, TradingIsAllowed, TradeNow, ManageProfitAndLoss и PrintOnChart. Эти методы инкапсулируют различные аспекты поведения советника, делая код более модульным и организованным.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ea.GetEma();
   ea.GetPositionsData();
   if(ea.TradingIsAllowed())
     {
      ea.TradeNow();
      ea.ManageProfitAndLoss();
     }
   ea.PrintOnChart();
  }

Я прикрепил полный исходный код советника ниже, это файл OOP_PriceActionEMA.mq5.


Тестируем наш советник в тестере стратегий

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

Вот настройки, которые мы применим в тестере стратегий:

  • Брокер: MetaQuotes-Demo (автоматически создается при установке MetaTrader 5)

  • Символ: EURJPY

  • Период тестирования (Интервал): 1 год и 2 месяца (январь 2023 - март 2024)

  • Моделирование: Все тики на основе реальных тиков

  • Депозит:$10 000

  • Плечо: 1:100

Настройки тестера для OOP_PriceActionEMA

Входные параметры для тестирования OOP_PriceActionEMA


Благодаря оптимизированным настройкам советника наша простая стратегия по ценовому действию показывает годовую прибыль в размере 41% при торговле со стартовым капиталом в размере 10 000 долларов США по паре EURJPY, с использованием кредитного плеча 1:100 и поддержанием низкой просадки капитала всего на уровне 5%. Эта стратегия многообещающая и может быть дополнительно улучшена другими индикаторами для достижения лучших результатов, особенно при одновременном запуске на нескольких символах.


График тестирования OOP_PriceActionEMA

Результаты тестирования OOP_PriceActionEMA

Результаты тестирования OOP_PriceActionEMA



Заключение

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

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

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

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

Спасибо за внимание! Желаю вам успехов в MQL5-разработке и торговле.



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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Sergey Voytsekhovsky
Sergey Voytsekhovsky | 11 апр. 2024 в 16:45
Здравствуйте! Планируете ли вы опубликовать переведенную на русский язык версию вашей статьи? Я прочитал статью с помощью Google-переводчика, и мне, как начинающему программисту, она очень понравилась. Хотелось бы прочитать ее в авторском переводе, так как Google переводит не только сам текст статьи, иногда теряя смысл фразы или определения, но и фрагменты кода, что плохо сказывается на восприятии. Спасибо за вашу работу!
Kelvin Muturi Muigua
Kelvin Muturi Muigua | 13 апр. 2024 в 17:21
Sergey Voytsekhovsky #:
Здравствуйте! Планируете ли вы опубликовать переведенную на русский язык версию вашей статьи? Я прочитал статью с помощью Google-переводчика, и мне, как начинающему программисту, она очень понравилась. Хотелось бы прочитать ее в авторском переводе, так как Google переводит не только сам текст статьи, иногда теряя смысл фразы или определения, но и фрагменты кода, что плохо сказывается на восприятии. Спасибо за вашу работу!

Здравствуйте, Сергей! Спасибо за интерес к статье! Я ценю ваш отзыв и рад, что он оказался полезным. Статья будет переведена на русский язык модератором, так как я не владею русским языком, так что следите за новостями! Я очень ценю ваше терпение.

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