Скачать MetaTrader 5

Написание советника в MQL5 с использованием объектно-ориентированного подхода

12 июля 2010, 17:55
Samuel Olowoyo
20
10 437

Введение

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

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

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


1. Объектно-ориентированный подход

Одна из вещей, которая делает новый язык MQL5 гораздо более мощным и надежным, чем MQL4 - это возможность использования объектно-ориентированного программирования (ООП).

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

Чтобы прояснить ситуацию, давайте рассмотрим упомянутые выше термины "класс" и объект.

  • КЛАСС. Класс является расширением структур данных, однако, наряду с данными, он также содержит и функции. Класс может содержать несколько переменных и функций, которые называются членами класса. Это и есть инкапсуляция данных и функций, которые манипулируют этими данными. Класс является гораздо более мощным, все функции, используемые в советнике, можно включить в класс. Единственное, что потребуется - добавить ссылку на функции класса, когда потребуется их использование в коде. Кстати, эта статья как раз об этом.
  • ОБЪЕКТ. Объект - это экземпляр класса. Когда класс создан, для того, чтобы его использовать, мы должны объявить экземпляр этого класса. Он и называется объектом. Другими словами, для создания объектов нам нужен класс.

1.1. Объявление класса

Класс содержит описание членов (свойств и функций/методов) объекта, который вы хотите создать из класса. Рассмотрим пример.

Если мы хотим создать объект, который будет иметь двери (doors), сиденья (sits), шины (tyres), вес (weight) и т.п., а также сможет начинать движение (start), переключать передачу (changegear), останавливаться (stop) и подавать звуковой сигнал (horn), нам нужно написать для этого класс. Все эти характеристики (doors, sits, tyres, weight, start, changegear, stopи horn) будут членами класса.

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

Для того чтобы объявить класс, мы используем ключевое слово class.

Основной формат класса имеет вид:

class class_name 
{
  access_keyword_1:
    members1;

  access_keyword_2:
    members2;
  ...
};

Здесь class_name является идентификатором класса, который мы хотим написать, members1 и members2 являются членами класса.

Ключевое слово access_keyword указывает права доступа к членам нашего класса. Спецификатор доступа может быть private, protected или public. Отметим, что мы пытаемся создать класс, который будет использоваться нами и другими, без раскрытия самой реализации. По этой причине необходимы права доступа.

В нашем классе могут быть некоторые члены, доступ к которым нужно запретить за пределами класса (закрытые члены). Они объявляются в секции private с использованием ключевого слова private или protected. Другие члены класса, доступ к которым мы хотим открыть извне (открытые члены), будут объявлены секции public, с использованием ключевого слова public. Теперь, наш новый класс CAR будет выглядеть следующим образом:

class CAR 
{
  private:
    int        doors;
    int        sits;
    int        tyres;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();
  ...
};

Наш класс CAR объявлен при помощи ключевого слова class. Этот класс содержит восемь членов класса, четыре из которых являются закрытыми (private), и четыре открытыми (public). В закрытой секции находятся члены классов данных, три из них целочисленного типа (int), и один вещественный (double).

В открытой секции класса находятся члены-функции. Две из них возвращают тип данных bool, две функции возвращают void. Как вы заметили, ключевые слова доступа (private, public, protected) всегда сопровождаются двоеточием. Объявление класса завершается точкой с запятой. Члены класса объявляются в соответствии с их типом данных.

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

Например, в объявлении класса:

class CAR 
{
    int        doors;
    int        sits;
    int        tyres;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();
  ...
};

все четыре члена класса, объявленные при помощи ключевого слова public, автоматически получают доступ к закрытым членам класса.

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

CAR Honda;

Или мы можем создать другой объект:

CAR Toyota;

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

Как видно, вы можете создать столько объектов класса, сколько захотите. Это одно из преимуществ объектно-ориентированного программирования.

Теперь, давайте рассмотрим подробнее формат класса в MQL5.

class class_name 
{
  private:
    members1;
    members2;
    members3;

  public:
    class_name()  //Конструктор;
    ~class_name() //Деструктор;
    Members4();
    Members5();

  protected:
    members6;
    members7;
};

В приведенной выше декларации класса class_name - имя класса. Этот класс имеет девять членов, но помимо этих девяти, есть два специальных члена класса.

Конструктор (constructor):
Конструктор (представленный как
class_name()) - является специальной функцией, которая вызывается автоматически при создании нового объекта типа данного класса.  Поэтому в этом случае, когда вы создаете объект этого класса:

class_name object;

конструктор class_name() вызывается автоматически.

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

Деструктор (destructor):
Второй специальный член класса представлен как ~class_name(). Деструктор класса записывается с тильдой (~) перед именем класса. Он вызывается автоматически при удалении объекта класса. Все члены класса, которые должны быть деинициализированы, деинициализируются на данном этапе и не имеет значения, объявлен ли деструктор явно или нет.

Данные класса (data members):
Данные класса могут иметь любой допустимый тип данных, типа класса или структуры. При объявлении переменных-данных класса можно использовать любой допустимый тип данных (int, double, string и т.п.), объект типа других классов или структур (например, структуру MqlTradeRequest в MQL5).

Функции класса (function members):
Это члены класса, которые используются для модификации данных класса и выполнения основных функций/методов класса. Возвращаемый тип функций-членов класса может иметь любой допустимый тип (bool, void, double, string и т.д.).

Private:
Члены класса, объявленные внутри данной секции, доступны только функциям класса. Они не являются доступными для других функций вне класса.

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

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

1.2. НАСЛЕДОВАНИЕ

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

Базовый класс:<

class base_class 
{
  private:
    members1;
    members2;
    members3;

  public:
    class_name()  //конструктор;
    ~class_name() //деструктор;
    Members4();
    Members5();

  protected:
    members6;
    members7;
};

Производный класс:

class new_class : access_keyword base_class 
{
  private:
    members8;

  public:
    new_class()  //конструктор;
    ~new_class() //деструктор;
    Members9();
};

Прежде чем рассматривать детали, приведем некоторые пояснения. Класс new_class получен из класса base_class с использованием двоеточия и ключевого слова access_keyword, как показано выше.

Теперь класс new_class, производный от base_class, имеет доступ (или наследует) и открытые (public) и защищенные члены класса, но не может (или не наследует) иметь доступ к членам базового класса, объявленных в секции private. Класс new_class может иметь другую реализацию методов/функций членов класса, которая отличается от той, которая реализована в классе base_class.

Если при создании класса путем наследования используется ключевое слово public, это означает, что открытые и закрытые члены базового класса будут наследованы как открытые и закрытые члены класса-потомка. Если используется ключевое слово protected, то открытые и закрытые члены базового класса будут наследованы как закрытые члены класса-потомка.

Важно отметить, что при создании нового объекта типа new_class (наследованного класса) перед вызовом конструктора класса new_class сначала вызывается конструктор базового класса base_class. При удалении объекта сначала вызывается деструктор класса new_class, затем деструктор базового класса base_class.

Для того чтобы лучше понять концепцию наследования, вернемся к нашему исходному классу CAR.

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    bool       start();
    void       changegear();
    void       stop();
    bool       horn();

  private:
    int        tyres;
};

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

class SALOON : public CAR 
{
  private:
    int        maxspeed;

  public:
    void       runathighspeed();
};

У нашего полученного класса SALOON есть два собственных члена класса, а также семь унаследованных членов (защищенных и открытых), унаследованных от класса CAR. Это означает, что как только объект типа SALOON будет создан, он будет иметь доступ к открытым функциям-членам класса CAR, таким как start(), changegear(), stop() и horn() наравне с собственным открытым методом runathighspeed(). В этом заключается наследование.

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

Хм! Я надеюсь, что черная пелена, окутывающая мистическую вещь, называемую "ООП" или "класс" постепенно исчезает. Если все это вам кажется не совсем понятным, можно расслабиться, выпить чашку кофе и вернуться  назад, чтобы начать все с начала. Это не так таинственно, как вы думаете...

Если вы здесь, я предполагаю, что вы следуете ходу объяснений. Я хочу, чтобы вы сказали, сколько еще классов вы можете создать на базе нашего класса CAR? Пожалуйста, мне нужен ответ. Я серьезно. Дайте всем им имена, напишите декларации и вышлите мне на почтовый ящик. Если вы сможете описать их все, с меня ужин... (я шучу?).

Теперь, когда вы готовы идти дальше, продолжим...

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

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

Вернемся к нашему классу CAR и унаследованному от него классу SALOON:

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    bool               start();
    virtual void        changegear(){return(0);}
    void               stop();
    bool               horn();

  private:
    int        tyres;
};
class SALOON : public CAR 
{
  private:
    int        maxspeed;

  public:
    void               runathighspeed();
    virtual  void       changegear(){gear1=reverse; gear2=low; gear3=high;}
  };

class WAGON : public CAR 
{
  private:
   bool               hasliftback;

  public:
   virtual  void       changegear(){gear1=low; gear2=high; gear3=reverse;}
};

Посмотрим на некоторые изменения, которые мы здесь сделали. Сначала, мы объявили новый класс (производный от класса CAR), называемый WAGON (пикап) с двумя членами. Также мы модифицировали функцию changegear() - теперь она объявлена виртуальной (virtual) функцией базового класса. Почему мы сделали функцию changegear() виртуальной? Просто потому, что мы хотим, чтобы любой класс, наследующий форму функции базового класса, мог бы иметь свою собственную реализацию.

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

В примерах, приведенных выше, классы SALOON (седан) и WAGON (пикап) имеют различные реализации функции changegear().

1.3. ОПРЕДЕЛЯЕМ МЕТОДЫ КЛАССА (ФУНКЦИИ)

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

class CAR 
{
  protected:
    int        doors;
    int        sits;
    double     weight;

  public:
    void       CAR() // Конструктор
    bool       start();
    void       changegear();
    void       stop();
    bool       horn(){нажать клаксон;}

  private:
    int        tyres;
};

 void CAR::CAR()
{
 // здесь инициализируются переменные классаe
}

bool CAR::start()
{
 // здесь процедура начала движения машины
}

void CAR::changegear()
{
// здесь процедура изменения передачи скоростей
}

void CAR::stop()
{
// здесь процедура остановки машины
}

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

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

1.4. ФУНКЦИИ

Кстати, что такое функция?

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

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

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

Функция может быть определена следующим образом:

Return_type function_name (parameters1,parameters2,…)
{
  Expressions; //(действия, производимые функцией)
}
  • Return_type : тип данных, возвращаемых функцией (должен быть указан корректный тип данных или тип void в случае, если функция ничего не возвращает)
  • Function_name: имя функции (должно быть правильно указано), которое будет использоваться для вызова функции.
  • Parameters: параметры - переменные, видимые внутри функции как локальные переменные. Если функция имеет более одного параметра, они разделяются запятыми.
  • Expressions: код функции, содержит блок операторов.

Пример функции:

int doaddition (int x, int y)
{
 return(x+y);
}

Функция возвращает тип integer(), имя функции "doaddition" (выполнить сложение), int x и int y - параметры. То, что делает функция - складывает значения двух переданных ей параметров и возвращает результат. Если передать этой функции две целые переменные 2 и 3, функция выполнит их сложение и в результате возвратит 5.

int doaddition(2,3) // возвращает 5

Для получения более подробной информации о функциях посмотрите справку MQL5.

Достаточно теории, перейдем к работе.

Цель этой статьи - научить вас тому, как написать класс для вашего советника с использованием объектно-ориентированного подхода, представленного в MQL5.

Приступим...


2. Пишем советник

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

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

Следующая вещь - выбор функционала для реализации нашего класса. Повторим нашу торговую стратегию из первой статьи:

Что будет делать наш советник:

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

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

   1. Мы будем использовать индикатор Moving Average (скользящие средние) с периодом 8 (вы можете выбрать любой период, но в данной стратегии мы будем использовать период 8).
2. Мы хотим, чтобы наш советник покупал, если 8-периодная скользящая средняя (далее для удобства будем называть ее MA-8) возрастает и текущая цена закрытия находится выше ее; советник должен продавать, когда MA-8 падает и цена закрытия находится ниже MA-8.
3. Также мы собираемся использовать другой индикатор, называемый Average Directional Movement (ADX) с периодом 8 для определения факта наличия тренда на рынке. Это нужно для того, чтобы входить в рынок, когда он находится в состоянии тренда. Для того чтобы это реализовать, мы будем помещать торговый запрос (на покупку или продажу) при наступлении условий, указанных выше, а также при значениях ADX, больших 22. Если ADX>22, но уменьшается или ADX<22, мы не будем помещать торговые запросы даже при наступлении условий, изложенных в пункте 2.
4. Мы хотим защитить себя установкой ордеров Stop Loss в 30 пунктов, Take Proft установим на уровне 100 пунктов.
5. Также мы хотим, чтобы советник проверял возможности для продажи/покупки только при формировании нового бара, при этом советник должен помещать ордер на покупку только в случае сигнала на покупку и отсутствия открытых длинных позиций. Аналогично в случае продажи - условия на продажу и отсутствие открытых коротких позиций.

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

Теперь понятно, что мы хотим сделать. Функции, которые мы хотим делегировать нашему классу, следующие:
  • Проверка условий покупки и продажи
  • Установка ордеров на покупку и продажу в зависимости от результатов проверки условий

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

    • Получение всех хэндлов индикаторов (в функции OnInit советника)
    • Получение всех индикаторных буферов (в функции OnTick советника)
    • Освобождение всех хэндлов индикаторов (в функции OnDeinit советника)

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

    • Получение периодов ADX и MA и других важных параметров, вроде периода графика и наименование инструмента.

    Для проверки свободной маржи перед помещением торгового запроса мы также включим:
    • Проверка свободной маржи/процента депозита в торговле

    При этом у нас уже есть представление о том, какие переменные и функции должны быть в нашем классе.

    Хорошо, с этим разобрались, время писать код...

    2.1. Пишем класс

    Давайте начнем с запуска MetaEditor (я надеюсь, вы его уже знаете). После открытия редактора, создадим новый MQL-документ, нажатием New  в панели инструментов или Ctrl+N.

    В окне Мастера MQL5 выберите "Include" и нажмите кнопку "Далее".

    Рисунок 1. Создание нового MQL5-документа

    Рисунок 1. Создание нового MQL5-документа

    Напишите имя файла, как показано ниже и нажмите "Завершить":

    Рисунок 2. Свойства включаемого файла

    Рисунок 2. Свойства включаемого файла

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

    Как обычно, редактор предоставил вам шаблон того, что, по его мнению, вы хотите сделать.


    Для начала, пожалуйста, удалите все строки, находящиеся ниже "#property link". У вас должно получиться следующее:
    //+------------------------------------------------------------------+
    //|                                              my_expert_class.mqh |
    //|                        Copyright 2010, MetaQuotes Software Corp. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2010, MetaQuotes Software Corp."
    #property link      "http://www.mql5.com"

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

    //+------------------------------------------------------------------+
    //| ОБЪЯВЛЕНИЕ КЛАССА                                                |
    //+------------------------------------------------------------------+
    class MyExpert
    {

    Проанализируем объявление класса. Объявление начинается с имени класса. Затем мы объявляем закрытые члены класса:

    //+------------------------------------------------------------------+
    //| ОБЪЯВЛЕНИЕ КЛАССА                                                |
    //+------------------------------------------------------------------+
    class MyExpert
    {
    //--- закрытые члены (переменные) класса
    private:
       int               Magic_No;   // Magic
       int               Chk_Margin; // Флаг необходимости проверки маржи перед размещением торгового запроса (1 или 0)
       double            LOTS;       // Количество лотов для торговли
       double            TradePct;   // Процент допустимой свободной маржи для торговли 
       double            ADX_min;    // Минимальное значение ADX
       int               ADX_handle; // Хэндл индикатора ADX
       int               MA_handle;  // Хэндл индикатора Moving Average
       double            plus_DI[];  // Массив для хранения значений +DI индикатора ADX
       double            minus_DI[]; // Массив для хранения значений -DI индикатора ADX
       double            MA_val[];   // Массив для хранения значений индикатора Moving Average
       double            ADX_val[];  // Массив для хранения значений индикатора ADX
       double            Closeprice; // Переменная для хранения цены закрытия предыдущего бара 
       MqlTradeRequest    trequest;   // Стандартная структура торгового запроса для отправки наших торговых запросов
       MqlTradeResult     tresult;    // Стандартная структура ответа торгового сервера для получения результатов торговых запросов
       string            symbol;     // Переменная для хранения имени текущего инструмента
       ENUM_TIMEFRAMES    period;     // Переменная для хранения текущего таймфрейма
       string            Errormsg;   // Переменная для хранения наших сообщений об ошибке
       int               Errcode;    // Переменная для хранения наших кодов ошибок

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

    Конструктор

    //--- Открытые члены/функции
    public:
       void              MyExpert();                                  //Конструктор класса

    Конструктор не имеет никаких входных параметров; пожалуйста, имейте это ввиду при написании своих классов.

    Функции класса

    //--- Открытые члены/функции (public)
    public:
       void              MyExpert();                                 //Конструктор класса
       void              setSymbol(string syb){symbol = syb;}         //Функция установки текущего символа
       void              setPeriod(ENUM_TIMEFRAMES prd){period = prd;} //Функция установки периода текущего символа
       void              setCloseprice(double prc){Closeprice=prc;}   //Функция установки цены закрытия предыдущего бара
       void              setchkMAG(int mag){Chk_Margin=mag;}          //Функция установки значения переменной Chk_Margin
       void              setLOTS(double lot){LOTS=lot;}               //Функция установки размера лота для торговли
       void              setTRpct(double trpct){TradePct=trpct/100;}   //Функция установки процента свободной маржи, используемой в торговле
       void              setMagic(int magic){Magic_No=magic;}         //Функция установки Magic number эксперта
       void              setadxmin(double adx){ADX_min=adx;}          //Функция установки минимального значения ADX

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

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

    Как и обычные функции, у них есть параметры допустимых типов данных. Надеюсь, это вам также известно.
    void              doInit(int adx_period,int ma_period);         //Функция, которая будет использоваться при инициализации советника
    void              doUninit();                                  //Функция, которая будет использоваться при деинициализации советника
    bool              checkBuy();                                  //Функция для проверки условий покупки
    bool              checkSell();                                 //Функция для проверки условий продажи
    void              openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL,
                             double TP,int dev,string comment="");   //Функция для открытия позиций на покупку
    void              openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL,
                              double TP,int dev,string comment="");  //Функция для открытия позиций на продажу

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

    Защищенные члены класса (protected)

    Эти члены класса будут унаследованы любым классом-потомком нашего класса. В них нет необходимости, если вы не собираетесь строить классы на базе данного класса. Вы также можете поместить их как закрытые (private) члены класса. Я делаю это лишь для того, чтобы вы знали различные особенности классов, которые мы обсуждали ранее.

    //--- Защищенные члены класса
    protected:
       void              showError(string msg, int ercode);   //Функция для отображения сообщений об ошибках
       void              getBuffers();                       //Функция для получения индикаторных буферов
       bool              MarginOK();                         //Функция проверки наличия достаточного количества маржи

    Эти три функции также очень важны для нашего класса. Функция showError будет выводить ошибки, а getBuffers будет использоваться для получения буферов индикаторов. Функция MarginOK проверяет наличие достаточного количества свободной маржи для открытия позиции.

    После того, как закончили с объявлением класса, не забудьте точку с запятой. Это очень важно.
    };   // конец объявления класса
    Следующее, что нужно сделать после объявления класса - определить функции класса, которые не были определены внутри объявления класса.
    //+------------------------------------------------------------------+
    // Определение функций-членов нашего класса
    //+------------------------------------------------------------------+
    /*
     Конструктор
    */
    void MyExpert::MyExpert()
    {
       //инициализация всех необходимых переменных
       ZeroMemory(trequest);
       ZeroMemory(tresult);
       ZeroMemory(ADX_val);
       ZeroMemory(MA_val);
       ZeroMemory(plus_DI);
       ZeroMemory(minus_DI);
       Errormsg="";
       Errcode=0;
    }

    Это наш конструктор класса. Здесь между именем класса и именем функции класса мы использовали оператор (::) (оператор разрешения контекста). Этим самым мы говорим следующее:

    Хотя функция определена вне объявления класса, она все еще находится в области видимости класса. Она является функцией-членом класса, имя которого указано перед оператором разрешения контекста (::).

    У конструктора нет никаких входных параметров. Здесь мы производим инициализацию большинства необходимых переменных класса, для их обнуления мы используем функцию ZeroMemory.

    void  ZeroMemory(
         void & variable      // обнуляемая переменная
       );

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

    Функция showError:

    /*
       Функция вывода сообщения об ошибке
    */
    void MyExpert::showError(string msg,int ercode)
    {
       Alert(msg, "-ошибка:", ercode, "!!");
    }
    

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

    Функция getBuffers:

    /*
     Получение индикаторных буферов
    */
    void MyExpert::getBuffers()
    {
       if(CopyBuffer(ADX_handle,0,0,3,ADX_val)<0 || CopyBuffer(ADX_handle,1,0,3,plus_DI)<0
          || CopyBuffer(ADX_handle,2,0,3,minus_DI)<0 || CopyBuffer(MA_handle,0,0,3,MA_val)<0)
         {
          Errormsg ="Ошибка копирования индикаторных буферов";
          Errcode = GetLastError();
          showError(Errormsg,Errcode);
         }
    }
    

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

    Для показа ошибок, которые могут возникнуть в процессе копирования буферов, мы используем нашу внутреннюю функцию ошибок ShowError.

    Функция MarginOK:

    /*
       Проверяет факт наличия достаточного количества маржи для торговли
    */
    bool MyExpert::MarginOK()
    {
       double one_lot_price;                                                   //Маржа, требуемая для одного лота
       double act_f_mag     = AccountInfoDouble(ACCOUNT_FREEMARGIN);               //Размер свободной маржи на счете
       long   levrage       = AccountInfoInteger(ACCOUNT_LEVERAGE);                //Плечо данного счета
       double contract_size = SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);  //Размер контракта
       string base_currency = SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);       //Базовая валюта
    
       if (base_currency == "USD") 
       {
          one_lot_price = contract_size/levrage;  
       }
       else
       {
          double bprice = SymbolInfoDouble(symbol,SYMBOL_BID);
          one_lot_price=bprice*contract_size/levrage;
       }
       // Проверка условия того, чтобы требуемое количество маржи не превышало заданный процент
       if (MathFloor(LOTS*one_lot_price) > MathFloor(act_f_mag*TradePct))
       {
          return(false);
       }
       else
       {
          return(true);
       }
    }
    

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

    Для получения размера свободной маржи счета мы используем функцию AccountInfoDouble() с идентификатором ENUM_ACCOUNT_INFO_DOUBLE.  Также для получения плеча счета мы используем функцию AccountInfoInteger() с идентификатором ENUM_ACCOUNT_INFO_INTEGER. Функции AccountInfoInteger() и AccountInfoDouble() предназначены для получения свойств текущего счета, на котором запущен советник.

    double  AccountInfoDouble(
       int  property_id      // идентификатор свойства
       );
    

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

    double  SymbolInfoDouble(
       string  name,        // наименование символа
       int     prop_id      // идентификатор свойства
       );
    

    Вычисления, которые мы производим, являются очень простыми.

    Для получения размера маржи, требуемого для торговли, мы рассматриваем две ситуации:

    1. Базовой валютой котировки является USD (USD/CAD, USD/CHF, USD/JPY, и т.д.)

    Margin required = Contract size per lot /Leverage

         2.  Базовой валютой котировки не является USD (EUR/USD, и т.д.)

    Margin required = current price of symbol * contract size per lot/Leverage.

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

    Функция doInit:

    //+-----------------------------------------------------------------------+
    // ОТКРЫТЫЕ(PUBLIC)ФУНКЦИИ НАШЕГО КЛАССА 
    //+-----------------------------------------------------------------------+
    /*
       Инициализация 
    */
    void MyExpert::doInit(int adx_period,int ma_period)
    {
       //--- Получаем хэндл индикатора ADX
       ADX_handle=iADX(symbol,period,adx_period);
      //--- Получаем хэндл индикатора Moving Average
       MA_handle=iMA(symbol,period,ma_period,0,MODE_EMA,PRICE_CLOSE);
      //--- Проверка корректности хэндлов
       if(ADX_handle<0 || MA_handle<0)
         {
          Errormsg="Ошибка создания хэндлов индикаторов";
          Errcode=GetLastError();
          showError(Errormsg,Errcode);
         }
    // Устанавливаем атрибут AsSeries для массивов
    // для значений ADX
       ArraySetAsSeries(ADX_val,true);
    // для значений +DI
       ArraySetAsSeries(plus_DI,true);
    // для значений -DI
       ArraySetAsSeries(minus_DI,true);
    // для значений MA
       ArraySetAsSeries(MA_val,true);
    }

    Эту открытую функцию мы собираемся использовать в функции OnInit() нашего советника, который мы скоро напишем. Она делает две вещи: cначала устанавливает хэндлы для наших индикаторов, а затем задает направление индексации массивов как в таймсериях. У этой функции два входных параметра, значения которых будут задаваться в коде нашего советника.

    Функция doUninit:

    /*
       Деинициализация
    */
    void MyExpert::doUninit()
    {
       //--- освобождаем наши индикаторные буферы
       IndicatorRelease(ADX_handle);
       IndicatorRelease(MA_handle);
    }

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

    Функция checkBuy:

    /*
       Проверка условий покупки
    */
    bool MyExpert::checkBuy()
    {
      /*
        Проверка открытия длинной позиции: скользящая средняя (MA) возрастает, 
        цена закрытия предыдущего бара выше ее, ADX > ADX min, +DI > -DI
      */
       getBuffers();
       //--- Объявляем переменные типа bool для хранения результатов проверки наших условий покупки
       bool Buy_Condition_1 = (MA_val[0]>MA_val[1]) && (MA_val[1]>MA_val[2]); // MA растет
       bool Buy_Condition_2 = (Closeprice > MA_val[1]);         // Цена закрытия предыдущего больше MA
       bool Buy_Condition_3 = (ADX_val[0]>ADX_min);             // Текущее значение ADX больше заданного минимального(22)
       bool Buy_Condition_4 = (plus_DI[0]>minus_DI[0]);         // +DI больше чем -DI
    //--- Собираем все вместе 
       if(Buy_Condition_1 && Buy_Condition_2 && Buy_Condition_3 && Buy_Condition_4)
         {
           return(true);
         }
         else
         {
           return(false);
         }
    }
    

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

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

    Первая вещь, которую мы здесь сделали - вызвали внутреннюю функцию класса getBuffers(), которая скопирует значения, используемые функцией checkBuy в соответствующие массивы.

    Подробнее об условиях покупки/продажи можно прочитать в статье "Пошаговое руководство по написанию MQL5-советников для начинающих".

    Функция checkSell:

    /*
       Проверка условий для продажи
    */
    bool MyExpert::checkSell()
    {
      /*
        Проверка условий продажи : скользящая средняя (MA) падает, 
        цена закрытия предыдущего бара ниже ее, ADX > ADX min, -DI > +DI
      */
      getBuffers();
      //--- Объявляем переменные типа bool для хранения результатов проверки условий для продажи
       bool Sell_Condition_1 = (MA_val[0]<MA_val[1]) && (MA_val[1]<MA_val[2]);  // MA падает
       bool Sell_Condition_2 = (Closeprice <MA_val[1]);                         // цена закрытия предыдущего бара меньше MA-8
       bool Sell_Condition_3 = (ADX_val[0]>ADX_min);                            // Текущее значение ADX больше, чем минимальное (22)
       bool Sell_Condition_4 = (plus_DI[0]<minus_DI[0]);                        // -DI больше, чем +DI
       
      //--- Собираем все вместе
       if(Sell_Condition_1 && Sell_Condition_2 && Sell_Condition_3 && Sell_Condition_4)
        {
           return (true);
        }
        else
        {
           return (false);
        }
    }

    Как и функция checkBuy, эта функция будет использоваться для проверки наличия условий для продажи. Поэтому тип возвращаемого ею значения bool - TRUE или FALSE. Это место, где мы задали нашу стратегию для продажи. Если справедливы условия для продажи, она вернет TRUE, если условие не выполняется, она вернет FALSE. При использовании этой функции в нашем коде, мы помещаем ордер на продажу в случае, если она вернула TRUE. Как и в функции checkBuy, сначала вызывается внутренняя функция getBuffers().

    Более подробно об условиях можно узнать в указанной выше статье.

    Функция openBuy:

    /*
       Открывает позицию на покупку
    */
    void MyExpert::openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL,double TP,int dev,string comment="")
    {
       // если необходимо проверять маржу
       if (Chk_Margin ==1) 
        {
          if (MarginOK()== false)
          {
             Errormsg= "У вас нет достаточного количества средств для открытия позиции!!!";
             Errcode =GetLastError();
             showError(Errormsg,Errcode);
          }
          else
          {
             trequest.action=TRADE_ACTION_DEAL;
             trequest.type=otype;
             trequest.volume=LOTS;
             trequest.price=askprice;
             trequest.sl=SL;
             trequest.tp=TP;
             trequest.deviation=dev;
             trequest.magic=Magic_No;
             trequest.symbol=symbol;
             trequest.type_filling=ORDER_FILLING_FOK;
             // отсылаем запрос
             OrderSend(trequest,tresult);
             // проверяем результат
             if(tresult.retcode==10009 || tresult.retcode==10008) //Запрос успешно выполнен 
              {
                Alert("Ордер Buy успешно размещен, тикет ордера #:",tresult.order,"!!");
              }
              else
              {
                Errormsg= "Запрос на установку ордера Buy не выполнен.";
                Errcode =GetLastError();
                showError(Errormsg,Errcode);
              } 
          }
       }
       else
       {
          trequest.action=TRADE_ACTION_DEAL;
          trequest.type=otype;
          trequest.volume=LOTS;
          trequest.price=askprice;
          trequest.sl=SL;
          trequest.tp=TP;
          trequest.deviation=dev;
          trequest.magic=Magic_No;
          trequest.symbol=symbol;
          trequest.type_filling=ORDER_FILLING_FOK;
          // отсылаем запрос
          OrderSend(trequest,tresult);
          // проверяем результат
          if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
           {
             Alert("Ордер Buy был успешно помещен, тикет ордера #:",tresult.order,"!!");
           }
           else
           {
             Errormsg= "Запрос на установку ордера Buy не выполнен";
             Errcode =GetLastError();
             showError(Errormsg,Errcode);
           }
        }  
    }

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

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

    Функция openSell:

    /*
       Открытие позиции на продажу
    */
    void MyExpert::openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL,double TP,int dev,string comment="")
    {
       // если необходимо проверять маржу
       if (Chk_Margin ==1) 
       {
          if (MarginOK()== false)
          {
             Errormsg= "У вас нет достаточного количества средств для открытия позиции!!!";
             Errcode =GetLastError();
             showError(Errormsg,Errcode);
          }
          else
          {
             trequest.action=TRADE_ACTION_DEAL;
             trequest.type=otype;
             trequest.volume=LOTS;
             trequest.price=bidprice;
             trequest.sl=SL;
             trequest.tp=TP;
             trequest.deviation=dev;
             trequest.magic=Magic_No;
             trequest.symbol=symbol;
             trequest.type_filling=ORDER_FILLING_FOK;
             // отсылаем запрос
             OrderSend(trequest,tresult);
             // проверяем результат
             if(tresult.retcode==10009 || tresult.retcode==10008) //Запрос успешно выполнен 
              {
                Alert("Ордер Sell успешно размещен, тикет ордера #:",tresult.order,"!!");
              }
              else
              {
                Errormsg= "Запрос на установку ордера Sell не выполнен";
                Errcode =GetLastError();
                showError(Errormsg,Errcode);
              }  
           } 
       }
       else
       {
          trequest.action=TRADE_ACTION_DEAL;
          trequest.type=otype;
          trequest.volume=LOTS;
          trequest.price=bidprice;
          trequest.sl=SL;
          trequest.tp=TP;
          trequest.deviation=dev;
          trequest.magic=Magic_No;
          trequest.symbol=symbol;
          trequest.type_filling=ORDER_FILLING_FOK;
          // отсылаем запрос
          OrderSend(trequest,tresult);
          // проверяем результат
          if(tresult.retcode==10009 || tresult.retcode==10008) //Request successfully completed 
           {
             Alert("Ордер Sell был успешно помещен, тикет ордера #:",tresult.order,"!!");
           }
           else
           {
             Errormsg= "Запрос на установку ордера Sell не выполнен";
             Errcode =GetLastError();
             showError(Errormsg,Errcode);
           }
       }  
    }

    Данная функция, так же, как и функция openBuy, будучи вызвана из нашего советника, открывает позицию на продажу. В качестве входных параметров указываются торговые параметры и некоторые переменные, которые будут переданы в коде нашего советника.

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

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

    Рисунок 3. Деструктор класса также показан в членах-функциях нашего класса

    Рисунок 3. Деструктор класса также показан в членах-функциях нашего класса

    Что же дальше?

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

    • либо вы рискуете нажимать кнопку "Отладка" в редакторе до тех пор, пока компилятор будет сообщать об ошибках (за исключением ошибки "no executable file produced’", поскольку файл .mqh является включаемым и не может быть скомпилирован в файл формата .ex5) или
    • идем дальше и пишем код советника, который будет использовать ваш класс. Как только вы начнете отладку советника, включаемый файл будет проверен вместе с ним. По сути, это наилучший и наиболее приемлемый путь контроля ошибок.

     Рисунок 4. Включаемые файлы .mqh files не могут быть скомпилированы

     Рисунок 4. Включаемые файлы .mqh files не могут быть скомпилированы

    2.2. ПИШЕМ СОВЕТНИК

    Я думаю, ваш редактор все еще открыт. Снова создайте новый документ, но на этот раз выберите "Советник" (подробности в статье). На этот раз назовите ваш советник "my_oop_ea".

    Вот что должно получиться:


    Теперь мы готовы к тому, чтобы написать наш советник на основе объектно-ориентированного подхода.

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

    Добавьте команду включения класса сразу же за последней директивой "#property".

    //+------------------------------------------------------------------+
    //|                                                    my_oop_ea.mq5 |
    //|                        Copyright 2010, MetaQuotes Software Corp. |
    //|                                              http://www.mql5.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2010, MetaQuotes Software Corp."
    #property link      "http://www.mql5.com"
    #property version   "1.00"
    // Include  our class
    #include <my_expert_class.mqh>

    Есть два способа включения файла:

    // Включаем файл с использованием скобок
    #include <my_expert_class.mqh>
    // Включаем файл при помощи кавычек
    #include "my_expert_class.mqh"

    Когда мы используем угловые скобки (< ... >), это означает, что файл включаемый файл следует брать из стандартной папки "\Include" (каталог \MQL5\Include\). Текущий каталог (в каталоге \MQL5\Experts) не рассматривается в качестве места поиска включаемого файла. Но если подключение файла осуществляется при помощи кавычек (" ... "), подключаемый файл ищется в текущем каталоге, при этом стандартная папка (каталог MQL5\Include) не проверяется.

    Если ваш класс сохранен в каталоге "\Include" (стандартная папка) и вы использовали кавычки вместо угловых скобок или наоборот, то при компиляции вы получите сообщение об ошибке.

    Рисунок 5. Сообщение об ошибке, появляющееся, когда не найден включаемый файл

    Рисунок 5. Сообщение об ошибке, появляющееся, когда не найден включаемый файл

    ВХОДНЫЕ ПАРАМЕТРЫ СОВЕТНИКА

    //--- входные параметры
    input int      StopLoss=30;      // Stop Loss
    input int      TakeProfit=100;   // Take Profit
    input int      ADX_Period=14;    // Период индикатора ADX
    input int      MA_Period=10;     // Период индикатора Moving Average
    input int      EA_Magic=12345;   // Magic Number советника
    input double   Adx_Min=22.0;     // Минимальное значение ADX
    input double   Lot=0.2;          // Количество лотов для торговли
    input int      Margin_Chk=0;    // Нужно ли проверять размер маржи перед помещением ордера (0=Нет, 1=Да)
    input double   Trd_percent=15.0; // Процент маржи, используемый в торговле

    Большинство входных параметров, указанных здесь, не являются новыми (см. статью). Мы обсудим лишь новые.

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

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

    Сразу же после входных параметров мы определяем два других параметра и затем создаем объект нашего класса для использования в коде нашего советника.

    Как указывалось выше, для создания объекта класса вы пишете имя класса и далее имя объекта, который желаете создать. Здесь мы создали объект Cexpert класса MyExpert.

    Объект Cexpert теперь может быть использован для доступа ко всем открытым (public) функциям класса MyExpert.

    Секция инициализации советника

    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- проверка наличия необходимого количества баров для работы
       if(Bars(_Symbol,_Period)<60) // если общее количество баров менее 60
         {
          Alert("У нас менее 60 баров, советник закончит работу!!");
          return(1);
         }
    //--- запуск функции инициализации
       Cexpert.doInit(ADX_Period,MA_Period);
    //--- установка всех необходимых переменных для нашего объекта класса
       Cexpert.setPeriod(_Period);     // задает период
       Cexpert.setSymbol(_Symbol);     // задает символ (валютную пару)
       Cexpert.setMagic(EA_Magic);    // задает Magic Number
       Cexpert.setadxmin(Adx_Min);    // устанавливаем минимальное значение ADX
       Cexpert.setLOTS(Lot);          // задаем кол-во лотов
       Cexpert.setchkMAG(Margin_Chk); // задаем флаг проверки маржи
       Cexpert.setTRpct(Trd_percent); // задаем минимальный процент необходимой свободной маржи
    //--- Включаем поддержку брокеров в 5 знаками
       STP = StopLoss;
       TKP = TakeProfit;
       if(_Digits==5 || _Digits==3)
         {
          STP = STP*10;
          TKP = TKP*10;
         }
    //---
       return(0);
      }

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

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

    Секция деинициализации советника

    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
    //--- Вызываем функцию деинициализации
       Cexpert.doUninit();
      }
    В нашем классе мы назвали функцию doUninit, поскольку она предназначена для того, чтобы освободить все хэндлы индикаторов, которые должны были быть созданы в секции инициализации советника.

    EA ONTICK SECTION

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
    //--- Имеем ли мы достаточное количество баров для работы
       int Mybars=Bars(_Symbol,_Period);
       if(Mybars<60) // если общее количество баров меньше 60
         {
          Alert("У нас меньше 60 баров, советник не будет работать!!");
          return;
         }
    
    //--- задаем некоторые структуры MQL5, которые будут использоваться в нашей торговле
       MqlTick latest_price;      // будет использоваться для получения текущих/последних котировок цены
       MqlRates mrate[];          // будет использоваться для хранения цен, объемов и спредов для каждого из баров
    /*
         Сделаем так, чтобы значения, которые мы будем использовать для массивов котировок
         имели индексацию как в таймсерии
    */
    // для массива котировок
       ArraySetAsSeries(mrate,true);

    Мы повторили проверку полного количества доступных баров. Затем мы объявили две переменные стандартных структур MQL5 (MqlTick и MqlRates). И, наконец, мы применили функцию ArraySetAsSeries для массива котировок.

    //--- Получаем последнюю цену котировки, используя структуру MqlTick 
       if(!SymbolInfoTick(_Symbol,latest_price))
         {
          Alert("Ошибка получения последней цены котировки - ошибка:",GetLastError(),"!!");
          return;
         }
    
    //--- Получим данные по последним трем барам 
       if(CopyRates(_Symbol,_Period,0,3,mrate)<0)
         {
          Alert("Ошибка копирования котировок/исторических данных - ошибка:",GetLastError(),"!!");
          return;
         }
    
    //--- советник должен проверять условия торговли только в случае начала нового бара
    // объявим static-переменную типа datetime
       static datetime Prev_time;
    // получим время начала текущего бара (бар 0)
       datetime Bar_time[1];
    // копируем время
       Bar_time[0] = mrate[0].time;
    // если оба времени равны, у нас нет нового бара
       if(Prev_time==Bar_time[0])
         {
          return;
         }
    // скопируем время в статическую переменную (сохраняем значение)
       Prev_time = Bar_time[0]; 
       

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

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

    //--- ошибок нет, продолжаем
    //--- есть ли у нас уже открытые позиции?
        bool Buy_opened = false, Sell_opened=false; // переменные для хранения результата проверки наличия открытых позиций
        
        if (PositionSelect(_Symbol) == true)  // у нас есть открытая позиция по текущему символу
        {
             if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
             {
                Buy_opened = true;  // Это длинная позиция
             }
             else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
             {
                Sell_opened = true; // Это короткая позиция
             }
        }

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

    // Скопируем цену закрытия предыдущего бара (бар 1) в соответствующую переменную эксперта
       Cexpert.setCloseprice(mrate[1].close);  // цена закрытия бара 1
    //--- Проверка наличия позиции на покупку
       if (Cexpert.checkBuy()==true)
       {
          if (Buy_opened) 
             {
                Alert("У нас уже есть позиция на покупку!!!"); 
                return;    // Не добавляем к длинной позиции
             }
          double aprice = NormalizeDouble(latest_price.ask,_Digits);
          double stl    = NormalizeDouble(latest_price.ask - STP*_Point,_Digits);
          double tkp    = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits);
          int    mdev   = 100;
          // размещаем ордер
          Cexpert.openBuy(ORDER_TYPE_BUY,Lot,aprice,stl,tkp,mdev);
       }

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

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

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

    //--- Проверка наличия позиции на продажу
       if (Cexpert.checkSell()==true)
       {
          // есть ли открытая позиция на продажу?
          if (Sell_opened) 
          {
             Alert("У нас уже есть открытая позиция на продажу!!!"); 
             return;    // Не добавляем к короткой позиции
          }
          double bprice = NormalizeDouble(latest_price.bid,_Digits);
          double bstl    = NormalizeDouble(latest_price.bid + STP*_Point,_Digits);
          double btkp    = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits);
          int    bdev   = 100;
          // размещаем ордер
          Cexpert.openSell(ORDER_TYPE_SELL,Lot,bprice,bstl,btkp,bdev);
       }
     
       return;
       } 

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

    Довольно просто, не так ли? Мы закончили с написанием кодов. Теперь пришло время отладить их. Если вы не знаете, как пользоваться отладчиком, пожалуйста, прочитайте статью.

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

    Рисунок 6. Наш включаемый файл подключен к советнику

    Рисунок 6. Наш включаемый файл подключен к советнику

    Если все нормально, работа сделана отлично. Теперь настало время проверить наш советник при помощи тестера стратегий. Мы должны скомпилировать наш советник перед тестированием в Тестере стратегий. Для того чтобы сделать это, нажмите на кнопку "Компилировать" или нажмите клавишу F7 на клавиатуре.

    Рисунок  7. Для компиляции нашего кода нажмите на кнопку "Компилировать"

    Рисунок  7. Для компиляции нашего кода нажмите на кнопку "Компилировать"

    В главном меню торгового терминала выберите Вид --> Тестер стратегий или нажмите Ctrl+R для запуска Тестера стратегий (подробности использования тестера в статье).

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

    Рисунок 8. Тестер выводит ошибку при выборе советника, который не скомпилирован

    Рисунок 8. Тестер выводит ошибку при выборе советника, который не скомпилирован

    Результаты тестирования нашего советника, созданного на основе объектно-ориентированного программирования, приведены ниже:

    Рисунок 9. Результаты торговли нашего советника, построенного на основе ООП

    Рисунок 9. Результаты торговли нашего советника, построенного на основе ООП


    График баланса:

    Рисунок 10. График баланса торговли нашего советника, построенного на основе ООП

    Рисунок 10. График баланса торговли нашего советника, построенного на основе ООП

    Отчет торговой активности советника в журнале:

    Рисунок 11. Результаты торговой активности нашего советника, построенного на основе ООП

    Рисунок 11. Результаты торговой активности нашего советника, построенного на основе ООП

    График сделок в тестере:

    Рисунок 12. График торговли нашего советника, построенного на основе ООП

    Рисунок 12. График торговли нашего советника, построенного на основе ООП


    Выводы

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

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

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


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

    Прикрепленные файлы |
    my_expert_class.mqh (14.99 KB)
    my_oop_ea.mq5 (6.87 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (20)
    o_o
    o_o | 22 дек 2010 в 17:26

    это обещали убрать
    Rashid Umarov
    Rashid Umarov | 22 дек 2010 в 17:27
    Jager:

    Вопросик.


    100 зелененькие - это в руб или у.е.? ;)

    Это значение таймаута в миллисекундах. Этот параметр уже убрали из всех торговых функций.
    VictorD
    VictorD | 17 апр 2014 в 17:05

    Помогите, пожалуйста, разобраться, что-то никак не догоняю :

    В самом начале в советнике вызывается функция

      Cexpert.doInit(ADX_Period,MA_Period);
    при этом для ее корректного выполнения требуются уже установленные параметры symbol и period :
    //+-----------------------------------------------------------------------+
    // ОТКРЫТЫЕ(PUBLIC)ФУНКЦИИ НАШЕГО КЛАССА 
    //+-----------------------------------------------------------------------+
    /*
       Инициализация 
    */
    void MyExpert::doInit(int adx_period,int ma_period)
    {
       //--- Получаем хэндл индикатора ADX
       ADX_handle=iADX(symbol,period,adx_period);
      //--- Получаем хэндл индикатора Moving Average
       MA_handle=iMA(symbol,period,ma_period,0,MODE_EMA,PRICE_CLOSE);
    однако, заполнение этих параметров конкретными значениями происходит позже, уже после Cexpert.doInit :
    //--- запуск функции инициализации
       Cexpert.doInit(ADX_Period,MA_Period);
    //--- установка всех необходимых переменных для нашего объекта класса
       Cexpert.setPeriod(_Period);     // задает период
       Cexpert.setSymbol(_Symbol);     // задает символ (валютную пару)
       
       Никак не пойму, как может правильно выполниться Cexpert.doInit, если переменной symbol пока не присвоено
    значение (или как-то присвоено ?) Застрял тут и дальше никак . Спасибо.
    Snaf
    Snaf | 18 апр 2014 в 07:56
    VictorD:

    Помогите, пожалуйста, разобраться, что-то никак не догоняю :

    В самом начале в советнике вызывается функция

    doInit сработал (вероятно по умолчанию этим переменным присвоено NULL и 0). setPeriod и setSymbol должны быть до Init.
    VictorD
    VictorD | 18 апр 2014 в 19:07
    Snaf:
    doInit сработал (вероятно по умолчанию этим переменным присвоено NULL и 0). setPeriod и setSymbol должны быть до Init.

    Понятно, спасибо

    Использование ORDER_MAGIC для торговли разными экспертами на одном инструменте Использование ORDER_MAGIC для торговли разными экспертами на одном инструменте

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

    Функции для управления капиталом в экспертах Функции для управления капиталом в экспертах

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

    Исследование быстродействия скользящих средних в MQL5 Исследование быстродействия скользящих средних в MQL5

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

    Как написать индикатор на основе другого индикатора Как написать индикатор на основе другого индикатора

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