Создание мульти-экспертов на основе торговых моделей

Vasiliy Sokolov | 8 декабря, 2010


Введение

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

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

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

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

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

Сторонние разработчики по-разному предлагают решить проблему работы с нетто-позицией - от написания специализированных менеджеров виртуальных ордеров (см. статью "Virtual Order Manager для управления ордерами в позициях терминала MetaTrader 5") до учета вклада в совокупную позицию с помощью магического номера (см. "Оптимальный метод подсчета объема совокупной позиции по заданному магическому номеру" или "Использование ORDER_MAGIC для торговли разными экспертами на одном инструменте").

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

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

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

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

Итак, вот основные задачи, которые нам потребуется решить:

  1. Эксперт должен торговать на основе сразу нескольких торговых систем. При этом  он должен одинаково легко торговать как по одной, так и по нескольким торговым системам;
  2. Все торговые системы, заложенные в эксперте, не должны конфликтовать друг с другом. Каждая торговая система должна обрабатывать только свой вклад в совокупную позицию, и только свои ордера;
  3. Любая из совокупности систем должна одинаково легко торговаться как на одном таймфрейме инструмента, так и на всех таймфреймах сразу.
  4. Любая из совокупности систем должна одинаково легко торговаться как на одном торговом инструменте, так и на всех доступных инструментах сразу.

Если изучить внимательно список тех задач, которые нам предстоит решить, мы придем к трехмерному массиву. Первое измерение массива - это количество торговых систем, второе измерение – количество таймфреймов, на которых должна работать конкретная ТС, третье – количество торговых инструментов для ТС. Простой подсчет показывает, что даже такой простой советник как MACD Sample, работая одновременно на 8 мажорных валютных парах, будет иметь 152 независимых решения: 1 советник * 8 пар * 19 таймфреймов (недельные и месячные таймфреймы не учитываются).

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


Термины и понятия

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

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

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


Полный отказ от учета совокупной позиции 

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

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

В результате этого совокупная позиция изменится, и будет составлять 1 длинный контракт. На этот раз она будет включать в себя вклад уже двух торговых систем. Далее в дело вступает третья торговая система и совершает короткую сделку с этим же активом, объемом один стандартный контракт. Совокупная позиция станет нейтральной, т.к. -3 short + 4 long - 1 short = 0.

Означает ли отсутствие совокупной позиции, что все три торговые системы не находятся в рынке? Вовсе нет. Две из них держат четыре контракта без покрытия, а это означает, что покрытие через некоторое время будет произведено. С другой стороны, третья система держит 4 длинных контракта, которые еще предстоит продать обратно. Лишь тогда, когда будет произведен полный выкуп четырех коротких контрактов и продажа с покрытием четырех длинных контрактов, нейтральная позиция будет означать реальное отсутствие позиции у всех трех систем.

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

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

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


Проектирование абстрактной торговой модели

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

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

Вот элементарный пример решения такой задачи:

class CModel
{
protected:
   string            m_name;
public:
   void               CModel(){m_name="Model base";}
   bool virtual      Processing(void){return(true);}
};

class cmodel_macd : public CModel
{
public:
   void              cmodel_macd(){m_name="MACD Model";}
   bool              Processing(){Print("Model name is ", m_name);return(true);}
};

class cmodel_moving : public CModel
{
public:
   void              cmodel_moving(){m_name="Moving Average";}
   bool              Processing(){Print("Model name is ", m_name);return(true);}
};

cmodel_macd     *macd;
cmodel_moving   *moving;

Давайте разберемся, как работает этот код. Базовый класс модели CModel содержит одну защищенную переменную типа string под названием m_name. Ключевое слово protected разрешает использовать эту переменную потомкам класса, поэтому в его производных эта переменная уже будет включена. Далее базовый класс определяет виртуальную функцию Processing(). Слово 'virtual' в данном случае говорит нам о том, что эта функция-обертка или интерфейс взаимодействия между экспертом и конкретным экземпляром модели.

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

Далее описываются два класса cmodel_macd и cmodel_moving. Оба порождены от класса CModel, поэтому оба обладают своими экземплярами функции Processing() и переменной m_name. Обратите внимание, что внутренняя реализация функции Processing у обеих моделей разная. В одной модели она состоит из вывода функции Print("It is cmodel_macd. Model name is ", m_name), во второй из Print("It is cmodel_moving. Model name is ", m_name). Далее создаются два указателя, каждый из которых может указывать на конкретный экземпляр модели, один на класс типа cmodel_macd, другой на cmodel_moving.

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

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

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

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

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

В штатную поставку MetaTrader 5 входит специальный вспомогательный класс CList, который предоставляет возможность работы со связанными списками. Однако элементом такого списка может быть только объект типа CObject, так как только он обладает специальными указателями для работы со связанными списками. Сам по себе класс CObject довольно примитивен, по своей сути это простой интерфейс для взаимодействия с классом CList.

В этом можно убедиться, взглянув на его реализацию:

//+------------------------------------------------------------------+
//|                                                       Object.mqh |
//|                      Copyright © 2010, MetaQuotes Software Corp. |
//|                                       https://www.metaquotes.net/ |
//|                                              Revision 2010.02.22 |
//+------------------------------------------------------------------+
#include "StdLibErr.mqh"
//+------------------------------------------------------------------+
//| Class CObject.                                                   |
//| Purpose: Base class element storage.                             |
//+------------------------------------------------------------------+
class CObject
  {
protected:
   CObject          *m_prev;               // previous list item
   CObject          *m_next;               // next list item

public:
                     CObject();
   //--- methods of access to protected data
   CObject          *Prev()                { return(m_prev); }
   void              Prev(CObject *node)   { m_prev=node;    }
   CObject          *Next()                { return(m_next); }
   void              Next(CObject *node)   { m_next=node;    }
   //--- methods for working with files
   virtual bool      Save(int file_handle) { return(true);   }
   virtual bool      Load(int file_handle) { return(true);   }
   //--- method of identifying the object
   virtual int       Type() const          { return(0);      }

protected:
   virtual int       Compare(const CObject *node,int mode=0) const { return(0); }
  };
//+------------------------------------------------------------------+
//| Constructor CObject.                                             |
//| INPUT:  no.                                                      |
//| OUTPUT: no.                                                      |
//| REMARK: no.                                                      |
//+------------------------------------------------------------------+
void CObject::CObject()
  {
//--- initialize protected data
   m_prev=NULL;
   m_next=NULL;
  }
//+------------------------------------------------------------------+

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

Теперь самое важное. Благодаря механизму наследования можно включить этот класс в свою торговую модель, а это значит, что класс торговой модели может быть включен в список типа CList! Давайте попробуем это сделать.

Итак, наш абстрактный класс CModel мы сделаем потомком класса CObject:

class CModel : public CObject

Так как наши классы cmodel_moving и cmodel_average наследуются от CModel, то и они включают в свой состав элементы CObject, следовательно, они могут быть включены в список типа CList.  Исходный код, который создает две условные торговые модели,  помещает их в список и последовательно опрашивает каждый тик, представлен ниже:

//+------------------------------------------------------------------+
//|                                            ch01_simple_model.mq5 |
//|                            Copyright 2010, Vasily Sokolov (C-4). |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, Vasily Sokolov (C-4)."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Arrays\List.mqh>

// Base model
class CModel:CObject
{
protected:
   string            m_name;
public:
        void              CModel(){m_name="Model base";}
        bool virtual      Processing(void){return(true);}
};

class cmodel_macd : public CModel
{
public:
   void              cmodel_macd(){m_name="MACD Model";}
   bool              Processing(){Print("Processing ", m_name, "...");return(true);}
};

class cmodel_moving : public CModel
{
public:
   void              cmodel_moving(){m_name="Moving Average";}
   bool              Processing(){Print("Processing ", m_name, "...");return(true);}
};

//Create list of models
CList *list_models;

void OnInit()
{
   int rezult;
   // Great two pointer
   cmodel_macd          *m_macd;
   cmodel_moving        *m_moving;
   list_models =        new CList();
   m_macd   =           new cmodel_macd();
   m_moving =           new cmodel_moving();
   //Check valid pointer
   if(CheckPointer(m_macd)==POINTER_DYNAMIC){
      rezult=list_models.Add(m_macd);
      if(rezult!=-1)Print("Model MACD successfully created");
      else          Print("Creation of Model MACD has failed");
   }
   //Check valid pointer
   if(CheckPointer(m_moving)==POINTER_DYNAMIC){
      rezult=list_models.Add(m_moving);
      if(rezult!=-1)Print("Model MOVING AVERAGE successfully created");
      else          Print("Creation of Model MOVING AVERAGE has failed");
   }
}

void OnTick()
{
   CModel               *current_model;
   for(int i=0;i<list_models.Total();i++){
      current_model=list_models.GetNodeAtIndex(i);
      current_model.Processing();
   }
}

void OnDeinit(const int reason)
{
   delete list_models;
}

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

2010.10.10 14:18:31     ch01_simple_model (EURUSD,D1)   Prosessing Moving Average...
2010.10.10 14:18:31     ch01_simple_model (EURUSD,D1)   Processing MACD Model...
2010.10.10 14:18:21     ch01_simple_model (EURUSD,D1)   Model MOVING AVERAGE was created successfully
2010.10.10 14:18:21     ch01_simple_model (EURUSD,D1)   Model MACD was created successfully  

Давайте подробно  разберем, как работает этот код. Итак, как уже было сказано выше, наша базовая торговая модель CModel наследуется от класса CObject, что дает нам право включать потомков базовой модели в список типа CList:

rezult=list_models.Add(m_macd);
rezult=list_models.Add(m_moving);

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

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

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

Опрос созданных моделей осуществляется в функции OnTick(). Он состоит из цикла for. В этом цикле определяется количество элементов, после чего происходит последовательный проход от первого элемента цикла (i=0) до последнего (i<list_models.Total();i++):

CModel               *current_model;
for(int i=0;i<list_models.Total();i++){
   current_model=list_models.GetNodeAtIndex(i);
   current_model.Processing();
}

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

Задачи, которые ложатся на "плечи" этой функции, обширны:

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

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

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

В целом систему взаимодействия, созданную нами, можно описать следующей схемой:

Обратите внимание, что хотя опрос моделей в представленном выше коде происходит внутри функции OnTick(), это не является принципиальным. Цикл опроса с легкостью можно поместить в любую другую требуемую функцию, например в OnTrade() или OnTimer()

 

Таблица виртуальных ордеров – основа модели

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

Как уже было сказано выше, новая парадигма нетто-позиции определяет иные правила работы с ордерами и сделками. В MetaTrader 4 каждая сделка сопровождалась своим ордером, который существовал на вкладке "Торговля" с момента его размещения и до самого момента отмены ордера или закрытия сделки, инициированной им же.

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

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

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

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

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

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

Давайте посмотрим, как устроен этот класс:

#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"

#include <Trade\_OrderInfo.mqh>
#include <Trade\_HistoryOrderInfo.mqh>
#include <Arrays\List.mqh>
class CTableOrders : CObject
{
private:
   ulong             m_magic;       // Магик эксперта выставившего ордер
   ulong             m_ticket;      // Тикет  основного ордера
   ulong             m_ticket_sl;    // Тикет ордера выкупа сделки, совершенной на основании основного ордера (Stop Loss)
   ulong             m_ticket_tp;    // Тикет ордера выкупа сделки, совершенной на основании основного ордера (Take Profit)
   ENUM_ORDER_TYPE   m_type;         // Тип основного ордера
   datetime          m_time_setup;  // Время установки ордера
   double            m_price;       // Цена ордера
   double            m_sl;          // Цена предполагаемого Stop Loss
   double            m_tp;          // Цена предполагаемого Take Profit
   double            m_volume_initial;      // Объем ордера
public:
                     CTableOrders();
   bool              Add(COrderInfo &order_info, double stop_loss, double take_profit);
   bool              Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit);
   double            StopLoss(void){return(m_sl);}
   double            TakeProfit(void){return(m_tp);}
   ulong             Magic(){return(m_magic);}
   ulong             Ticket(){return(m_ticket);}
   int               Type() const;
   datetime          TimeSetup(){return(m_time_setup);}
   double            Price(){return(m_price);}
   double            VolumeInitial(){return(m_volume_initial);}
};

CTableOrders::CTableOrders(void)
{
   m_magic=0;
   m_ticket=0;
   m_type=0;
   m_time_setup=0;
   m_price=0.0;
   m_volume_initial=0.0;
}

bool CTableOrders::Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit)
{
   if(HistoryOrderSelect(history_order_info.Ticket())){
      m_magic=history_order_info.Magic();
      m_ticket=history_order_info.Ticket();
      m_type=history_order_info.Type();
      m_time_setup=history_order_info.TimeSetup();
      m_volume_initial=history_order_info.VolumeInitial();
      m_price=history_order_info.PriceOpen();
      m_sl=stop_loss;
      m_tp=take_profit;
      return(true);
   }
   else return(false);
}

bool CTableOrders::Add(COrderInfo &order_info, double stop_loss, double take_profit)
{
   if(OrderSelect(order_info.Ticket())){
      m_magic=order_info.Magic();
      m_ticket=order_info.Ticket();
      m_type=order_info.Type();
      m_time_setup=order_info.TimeSetup();
      m_volume_initial=order_info.VolumeInitial();
      m_price=order_info.PriceOpen();
      m_sl=stop_loss;
      m_tp=take_profit;
      return(true);
   }
   else return(false);
}

int   CTableOrders::Type() const
{
   return((ENUM_ORDER_TYPE)m_type);
}

Подобно классу CModel, класс таблицы ордеров наследуется от CObject. Так же как и классы моделей, мы будем помещать экземпляры CTableOrders в специально созданный для этих целей список ListTableOrders типа CList.

Помимо собственно тикета ордера (m_tiket) класс содержит информацию о magic номере (ORDER_MAGIC) выставившего его эксперта, его типа, цены открытия, объема и уровнях предполагаемого перекрытия ордера: stop loss (m_sl) и take profit (m_tp). О последних двух значениях необходимо поговорить отдельно. Очевидно, что любая сделка должна быть рано или поздно закрыта противоположенной сделкой. Противоположная сделка может быть инициирована на основании текущей рыночной ситуации или безоговорочного выхода из сделки по заранее оговоренным ценам, в момент ее заключения.

В MetaTrader 4 такими "безоговорочными выходами из сделки" являются специальные типы выхода: Stop Loss и Take Profit. Особенностью MetaTrader 4 является то обстоятельство, что эти уровни действуют в отношении конкретного ордера. Например, если сработает стоп одного из активных ордеров, это никак не отразиться на прочих открытых ордерах по этому инструменту.

В MetaTrader 5 все обстоит несколько иначе. Хотя для каждого установленного ордера, помимо прочего, можно указать цену Stop Loss и Take Profit, эти уровни будут действовать не в отношении конкретного ордера, в котором эти цены были указаны, а в отношении целой позиции по этому инструменту.

Допустим, имеется открытая BUY позиция по EURUSD объемом 1 стандартный лот без уровней Stop Loss и Take Profit. Некоторое время спустя по EURUSD выставляется дополнительный ордер на покупку 0.1 лота с выставленными в нем уровнями Stop Loss и Take Profit - допустим каждый на расстоянии 100 пунктов от текущей цены. Через некоторое время цена достигнет либо уровня Stop Loss, либо уровня Take Profit. Когда это произойдет, то вся позиция размером 1.1 лота по EURUSD будет закрыта.

Иными словами, Stop Loss и Take Profit можно выставить только в отношении совокупной позиции, а не в отношении конкретного ордера. Исходя из этого, становится невозможным использовать эти ордера в мультисистемных экспертах. Это очевидно, так как если одна система выставит свой собственный Stop Loss и Take Profit, то он будет действовать для всех прочих систем, интерес которых уже учтен в совокупной позиции этого инструмента!

Следовательно, каждая из подсистем торгового эксперта должна использовать только свои, внутренние Stop Loss и Take Profit для каждого ордера в отдельности. Также к этому можно прийти исходя из того, что даже внутри одной торговой системы разные ордера могут иметь разные уровни Stop Loss и Take Profit, а как уже сказано выше, в MetaTrader 5 эти выходы нельзя назначить в отношении отдельных ордеров.

Если в таблице виртуальных ордеров разместить синтетические уровни Stop Loss и Take Profit, то эксперт сможет самостоятельно перекрывать действующие ордера  в момент, когда цена достигнет либо превзойдет эти уровни. После перекрытия этих ордеров их можно смело удалить из списка активных ордеров. Как это делается, будет описано ниже.

Класс CTableOrders, помимо собственно данных, содержит крайне важную функцию Add(). Этой функции передается тикет ордера, информацию о котором необходимо занести в таблицу. Помимо тикета ордера этой функции передаются уровни виртуальных Stop Loss и Take Profit. Сначала функция Add() пытается выделить ордер среди исторических ордеров, хранящихся на сервере. Если это ей удается, то она заносит информацию о тикете в экземпляр класса history_order_info, после чего начинает заносить информацию через него в новый элемент СTableOrders. Далее этот элемент заносится в список ордеров. Если выделение ордера не удалось, то, возможно, мы имеем дело с отложенным ордером, поэтому производится попытка выделить ордер из текущих ордеров посредством функции OrderSelect(). В случае успешного выбора этого ордера производятся те же действия, что и для исторического ордера.

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


СModel – базовый класс торговой модели

Итак, когда таблица ордеров спроектирована, настала пора описать полную версию нашей базовой модели CModel:

class CModel : public CObject
{
protected:
   long              m_magic;
   string            m_symbol;
   ENUM_TIMEFRAMES   m_timeframe;
   string            m_model_name;
   double            m_delta;
   CTableOrders      *table;
   CList             *ListTableOrders;
   CAccountInfo      m_account_info;
   CTrade            m_trade;
   CSymbolInfo       m_symbol_info;
   COrderInfo        m_order_info;
   CHistoryOrderInfo m_history_order_info;
   CPositionInfo     m_position_info;
   CDealInfo         m_deal_info;
   t_period          m_timing;
public:
                     CModel()  { Init();   }
                     ~CModel() { Deinit(); }
   string            Name(){return(m_model_name);}
   void              Name(string name){m_model_name=name;}
   ENUM_TIMEFRAMES    Timeframe(void){return(m_timeframe);}
   string            Symbol(void){return(m_symbol);}
   void              Symbol(string set_symbol){m_symbol=set_symbol;}
   bool virtual      Init();
   void virtual      Deinit(){delete ListTableOrders;}
   bool virtual      Processing(){return (true);}
   double            GetMyPosition();
   bool              Delete(ENUM_TYPE_DELETED_ORDER);
   bool              Delete(ulong Ticket);
   void              CloseAllPosition();
   //bool virtual      Trade();
protected:
   bool              Add(COrderInfo &order_info, double stop_loss, double take_profit);
   bool              Add(CHistoryOrderInfo &history_order_info, double stop_loss, double take_profit);

   void              GetNumberOrders(n_orders &orders);
   bool              SendOrder(string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, ulong ticket, double lot,
                              double price, double stop_loss, double take_profit, string comment);
};

Данные этого класса содержат основополагающие константы любой торговой модели.

Это magic номер (m_magic), символ на котором модель будет запущена (m_symbol), таймфрейм (m_timeframe), имя самой торговой модели (m_name).

Помимо этого, модель включает в себя уже знакомый нам класс таблицы ордеров (CTableOrders *table) и список, в котором экземпляры этой таблицы будут храниться, по одному экземпляру на каждый ордер (CList *ListTableOrders). Так как все данные будут создаваться динамически, по мере надобности, то работа с этими данными будет производиться через указатели.

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

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

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

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

Например, некоторые события нужно вызывать каждый новый бар. Для модели, торгующей на часовой графике, такие события должны вызываться каждый час, для модели, торгующей на дневной графике – каждый новый дневной бар. Ясно, что у этих моделей разные временные настройки и каждая должна храниться соответственно в своей модели. Структура t_period, включенная в класс CModel, позволяет хранить эти настройки отдельно, каждую в своей модели.

Вот как выглядит эта структура:

struct t_period
{
   datetime m1;
   datetime m2;
   datetime m3;
   datetime m4;
   datetime m5;
   datetime m6;
   datetime m10;
   datetime m12;
   datetime m15;
   datetime m20;
   datetime m30;
   datetime h1;
   datetime h2;
   datetime h3;
   datetime h4;
   datetime h6;
   datetime h8;
   datetime h12;
   datetime d1;
   datetime w1;
   datetime mn1;  
   datetime current; 
};

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

Вот функция, которая работает по описанному алгоритму:

bool timing(string symbol, ENUM_TIMEFRAMES tf, t_period &timeframes)
{
   int rez;
   MqlRates raters[1];
   rez=CopyRates(symbol, tf, 0, 1, raters);
   if(rez==0)
   {
      Print("Error timing");
      return(false);
   }
   switch(tf){
      case PERIOD_M1:
         if(raters[0].time==timeframes.m1)return(false);
         else{timeframes.m1=raters[0].time; return(true);}
      case PERIOD_M2:
         if(raters[0].time==timeframes.m2)return(false);
         else{timeframes.m2=raters[0].time; return(true);}
      case PERIOD_M3:
         if(raters[0].time==timeframes.m3)return(false);
         else{timeframes.m3=raters[0].time; return(true);}
      case PERIOD_M4:
         if(raters[0].time==timeframes.m4)return(false);
         else{timeframes.m4=raters[0].time; return(true);}
     case PERIOD_M5:
         if(raters[0].time==timeframes.m5)return(false);
         else{timeframes.m5=raters[0].time; return(true);}
     case PERIOD_M6:
         if(raters[0].time==timeframes.m6)return(false);
         else{timeframes.m6=raters[0].time; return(true);}
     case PERIOD_M10:
         if(raters[0].time==timeframes.m10)return(false);
         else{timeframes.m10=raters[0].time; return(true);}
     case PERIOD_M12:
         if(raters[0].time==timeframes.m12)return(false);
         else{timeframes.m12=raters[0].time; return(true);}
     case PERIOD_M15:
         if(raters[0].time==timeframes.m15)return(false);
         else{timeframes.m15=raters[0].time; return(true);}
     case PERIOD_M20:
         if(raters[0].time==timeframes.m20)return(false);
         else{timeframes.m20=raters[0].time; return(true);}
     case PERIOD_M30:
         if(raters[0].time==timeframes.m30)return(false);
         else{timeframes.m30=raters[0].time; return(true);}
     case PERIOD_H1:
         if(raters[0].time==timeframes.h1)return(false);
         else{timeframes.h1=raters[0].time; return(true);}
     case PERIOD_H2:
         if(raters[0].time==timeframes.h2)return(false);
         else{timeframes.h2=raters[0].time; return(true);}
     case PERIOD_H3:
         if(raters[0].time==timeframes.h3)return(false);
         else{timeframes.h3=raters[0].time; return(true);}
     case PERIOD_H4:
         if(raters[0].time==timeframes.h4)return(false);
         else{timeframes.h4=raters[0].time; return(true);}
     case PERIOD_H6:
         if(raters[0].time==timeframes.h6)return(false);
         else{timeframes.h6=raters[0].time; return(true);}
     case PERIOD_H8:
         if(raters[0].time==timeframes.h8)return(false);
         else{timeframes.h8=raters[0].time; return(true);}
     case PERIOD_H12:
         if(raters[0].time==timeframes.h12)return(false);
         else{timeframes.h12=raters[0].time; return(true);}
     case PERIOD_D1:
         if(raters[0].time==timeframes.d1)return(false);
         else{timeframes.d1=raters[0].time; return(true);}
     case PERIOD_W1:
         if(raters[0].time==timeframes.w1)return(false);
         else{timeframes.w1=raters[0].time; return(true);}
     case PERIOD_MN1:
         if(raters[0].time==timeframes.mn1)return(false);
         else{timeframes.mn1=raters[0].time; return(true);}
     case PERIOD_CURRENT:
         if(raters[0].time==timeframes.current)return(false);
         else{timeframes.current=raters[0].time; return(true);}
     default:
         return(false);
   }
}

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

Вот исходный код этой функции:

int GetPeriodEnumerator(uchar n_period)
{
   switch(n_period)
   {
      case 0: return(PERIOD_CURRENT);
      case 1: return(PERIOD_M1);
      case 2: return(PERIOD_M2);
      case 3: return(PERIOD_M3);
      case 4: return(PERIOD_M4);
      case 5: return(PERIOD_M5);
      case 6: return(PERIOD_M6);
      case 7: return(PERIOD_M10);
      case 8: return(PERIOD_M12);
      case 9: return(PERIOD_M15);
      case 10: return(PERIOD_M20);
      case 11: return(PERIOD_M30);
      case 12: return(PERIOD_H1);
      case 13: return(PERIOD_H2);
      case 14: return(PERIOD_H3);
      case 15: return(PERIOD_H4);
      case 16: return(PERIOD_H6);
      case 17: return(PERIOD_H8);
      case 18: return(PERIOD_H12);
      case 19: return(PERIOD_D1);
      case 20: return(PERIOD_W1);
      case 21: return(PERIOD_MN1);
      default:
         Print("Enumerator period must be smallest 22");
         return(-1);
   }
}

Все эти функции удобно объединить в единый файл в папке \Include. Назовем его Time.mqh.

Именно его будет включать в наш базовый класс CModel:

#incude <Time.mqh>

Помимо простых функций get/set типа Name(), Timeframe() и Symbol(), класс CModel содержит сложные функции типа  Init(), GetMyPosition(), Delete(), CloseAllPosition() и Processing(). О назначении последней функции вам должно быть уже известно, более подробно об ее внутреннем устройстве мы поговорим позже, а пока начнем описание с основных функций базового класса CModel.

Функция CModel::Add() динамически создает экземпляр класса CTableOrders, после чего заполняет его с помощью соответствующей функции CTabeOrders::Add(). Принцип ее работы был описан выше. После заполнения этот элемент включается в общий список всех ордеров текущей модели (ListTableOrders.Add(t)).

Функция CModel::Delete(), напротив, удаляет элемент типа CTableOrders из списка действующих ордеров. Для этого необходимо указать тикет ордера, который необходимо удалить. Принцип ее работы прост. Функция последовательно перебирает всю таблицу ордеров в поисках ордера с нужным тикетом. Если она находит такой ордер, то удаляет его.

Функция CModel::GetNumberOrders() подсчитывает количество действующих ордеров. Она заполняет специальную структуру n_orders:

struct n_orders
{
   int all_orders;
   int long_orders;
   int short_orders;
   int buy_sell_orders;
   int delayed_orders;
   int buy_orders;
   int sell_orders;
   int buy_stop_orders;
   int sell_stop_orders;
   int buy_limit_orders;
   int sell_limit_orders;
   int buy_stop_limit_orders;
   int sell_stop_limit_orders;
};

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

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

Давайте ознакомимся с исходным кодом этой функции:

bool CModel::SendOrder(string symbol, ENUM_ORDER_TYPE op_type, ENUM_ORDER_MODE op_mode, ulong ticket, 
                          double lot, double price, double stop_loss, double take_profit, string comment)
{
   ulong code_return=0;
   CSymbolInfo symbol_info;
   CTrade      trade;
   symbol_info.Name(symbol);
   symbol_info.RefreshRates();
   mm send_order_mm;
   
   double lot_current;
   double lot_send=lot;
   double lot_max=m_symbol_info.LotsMax();
   //double lot_max=5.0;
   bool rez=false;
   int floor_lot=(int)MathFloor(lot/lot_max);
   if(MathMod(lot,lot_max)==0)floor_lot=floor_lot-1;
   int itteration=(int)MathCeil(lot/lot_max);
   if(itteration>1)
      Print("The order volume exceeds the maximum allowed volume. It will be divided into ", itteration, " transactions");
   for(int i=1;i<=itteration;i++)
   {
      if(i==itteration)lot_send=lot-(floor_lot*lot_max);
      else lot_send=lot_max;
      for(int i=0;i<3;i++)
      {
         //Print("Send Order: TRADE_RETCODE_DONE");
         symbol_info.RefreshRates();
         if(op_type==ORDER_TYPE_BUY)price=symbol_info.Ask();
         if(op_type==ORDER_TYPE_SELL)price=symbol_info.Bid();
         m_trade.SetDeviationInPoints(ulong(0.0003/(double)symbol_info.Point()));
         m_trade.SetExpertMagicNumber(m_magic);
         rez=m_trade.PositionOpen(m_symbol, op_type, lot_send, price, 0.0, 0.0, comment); 
         // Засыпание не удалять и не перемещать! Иначе ордер не успеет попасть в m_history_order_info!!!
         Sleep(3000);
         if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||
            m_trade.ResultRetcode()==TRADE_RETCODE_DONE_PARTIAL||
            m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
         {
               //Print(m_trade.ResultComment());
               //rez=m_history_order_info.Ticket(m_trade.ResultOrder());
               if(op_mode==ORDER_ADD){
                  rez=Add(m_trade.ResultOrder(), stop_loss, take_profit);
               }
               if(op_mode==ORDER_DELETE){
                  rez=Delete(ticket);
               }
               code_return=m_trade.ResultRetcode();
               break;
         }
         else
         {
            Print(m_trade.ResultComment());
         }
         if(m_trade.ResultRetcode()==TRADE_RETCODE_TRADE_DISABLED||
            m_trade.ResultRetcode()==TRADE_RETCODE_MARKET_CLOSED||
            m_trade.ResultRetcode()==TRADE_RETCODE_NO_MONEY||
            m_trade.ResultRetcode()==TRADE_RETCODE_TOO_MANY_REQUESTS||
            m_trade.ResultRetcode()==TRADE_RETCODE_SERVER_DISABLES_AT||
            m_trade.ResultRetcode()==TRADE_RETCODE_CLIENT_DISABLES_AT||
            m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_ORDERS||
            m_trade.ResultRetcode()==TRADE_RETCODE_LIMIT_VOLUME)
         {
            break;
         }
      }
   }
   return(rez);
}

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

Рассмотрим такой случай: имеется ограничение на размер торговой позиции в 15 стандартных лотов в обоих направлениях. Текущая позиция длинная и равна 3 лотам. Торговая модель на основе своей системы управления капиталом хочет открыть длинную позицию объемом 18.6 лотов.  Функция CheckLot() вернет скорректированное значение объема сделки. В данном случае оно будет равно 12 лотам (т.к. 3 лота из 15 возможных уже заняты другими сделками). Если бы текущая открытая  позиция  была короткой а не длинной, то функция вернула бы 15 лотов вместо 18.6. Это максимально возможный объем позиции.

После выставления 15 лотов buy, совокупная позиция в этом случае будет 12 лотов (3 - sell, 15 - buy). Когда другая модель перекроет свою первоначальную короткую позицию 3 лотами buy, общая позиция станет максимально возможной – 15 лотов. Другие сигналы на покупку не будут обрабатываться, пока модель не перекроет частично или полностью свои 15 лотов buy. Возможный объем для запрашиваемой сделки исчерпан, функция вернет константу EMPTY_VALUE, такой сигнал нужно пропустить.

Если проверка возможности установленного объема прошла успешно, тогда делаются расчеты по величине требуемой маржи. Средств на счете может быть не достаточно для заявленного объема. Для этих целей существует функция CheckMargin(). Если маржи недостаточно, она попробует скорректировать заявленный объем сделки таким образом, чтобы текущая свободная маржа позволяла ее открыть. Если маржи недостаточно даже для открытия минимального объема, то мы находимся в состоянии Margin-Call.

Если в текущий момент нет никаких сделок, и маржа не используется, то это означает только одно – технический margin-call - состояние, когда невозможно открыть ни одной сделки. Без пополнения счета дальнейшее продолжение невозможно. Если какая-то маржа все-таки используется, то не остается ничего другого, как ждать, когда сделка, использующая эту маржу, будет закрыта. В любом случае при недостатке маржи будет возвращена константа EMPTY_VALUE.

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

Сначала рассчитывается количество ордеров, необходимых для реализации заявленного объема. Если заявленный объем не превышает максимально возможный объем сделки, то потребуется только один проход для выставления этого ордера. Если желаемый объем сделки выше максимально возможной, то этот объем разбивается на несколько частей. Например, требуется купить 11.3 лота EURUSD. Максимальный размер сделки по этому инструменту составляет 5.0 лотов. Тогда функция OrderSend самостоятельно разобьет этот объем на три ордера: 1 ордер – 5.0 лотов, 2 ордер – 5.0 лотов, 3 ордер - 1.3 лота.

Таким образом, вместо одного ордера будет выставлено целых три. Каждый из них будет занесен в таблицу ордеров, и будет иметь свои независимые параметры, например, величины виртуальных Stop Loss и Take Profit, magic номер и прочие параметры. В обработке таких ордеров не будет никаких сложностей, так как торговые модели спроектированы таким образом, чтобы они могли обрабатывать любое количество ордеров в их списках.

Действительно, все ордера будут иметь одинаковые величины Take Profit и Stop Loss. Каждый из них будет последовательно перебран функциями LongClose и ShortClose. Как только наступят подходящие условия для их закрытия или наступят их пороговые уровни SL и TP, все они будут закрыты.

Каждый ордер отправляется на сервер с помощью функции OrderSend класса Trade. Самый интересный нюанс работы скрыт ниже.

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

Если тип ордера, который требуется разместить, является ADD_ORDER, т.е. это самостоятельный ордер, который необходимо разместить в таблице ордеров, тогда функция добавляет информацию об этом ордере в таблицу ордеров. Если ордер выставляется для перекрытия ранее выставленного ордера (например, наступил виртуальный stop-loss), значит, он должен иметь тип DELETE_ORDER. После его выставления функция OrderSend самостоятельно удалит информацию об ордере, с которым он связан из списка ордеров. Для этого функции, помимо типа ордера, передается тикет ордера, с которым он связан. Если это ADD_ORDER, то тикет можно заполнить простым нулем.


Первая торговая модель на основе пересечения скользящих средних

Мы рассмотрели все самые важные элементы базового класса CModel. Настало время перейти к рассмотрению конкретного торгового класса.

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

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

#include <Models\Model.mqh>
#include <mm.mqh>
//+----------------------------------------------------------------------+
//| This model uses MACD indicator.                                      |
//| Buy when it crosses the zero line downward                           |
//| Sell when it crosses the zero line upward                            |
//+----------------------------------------------------------------------+  
struct cmodel_macd_param
{
   string            symbol;
   ENUM_TIMEFRAMES   timeframe;
   int               fast_ema;
   int               slow_ema;
   int               signal_ema;
};
   
class cmodel_macd : public CModel
{
private:
   int               m_slow_ema;
   int               m_fast_ema;
   int               m_signal_ema;
   int               m_handle_macd;
   double            m_macd_buff_main[];
   double            m_macd_current;
   double            m_macd_previous;
public:
                     cmodel_macd();
   bool              Init();
   bool              Init(cmodel_macd_param &m_param);
   bool              Init(string symbol, ENUM_TIMEFRAMES timeframes, int slow_ma, int fast_ma, int smothed_ma);
   bool              Processing();
protected:
   bool              InitIndicators();
   bool              CheckParam(cmodel_macd_param &m_param);
   bool              LongOpened();
   bool              ShortOpened();
   bool              LongClosed();
   bool              ShortClosed();
};

cmodel_macd::cmodel_macd()
{
   m_handle_macd=INVALID_HANDLE;
   ArraySetAsSeries(m_macd_buff_main,true);
   m_macd_current=0.0;
   m_macd_previous=0.0;
}
//this default loader
bool cmodel_macd::Init()
{
   m_magic      = 148394;
   m_model_name =  "MACD MODEL";
   m_symbol     = _Symbol;
   m_timeframe  = _Period;
   m_slow_ema   = 26;
   m_fast_ema   = 12;
   m_signal_ema = 9;
   m_delta      = 50;
   if(!InitIndicators())return(false);
   return(true);
}

bool cmodel_macd::Init(cmodel_macd_param &m_param)
{
   m_magic      = 148394;
   m_model_name = "MACD MODEL";
   m_symbol     = m_param.symbol;
   m_timeframe  = (ENUM_TIMEFRAMES)m_param.timeframe;
   m_fast_ema   = m_param.fast_ema;
   m_slow_ema   = m_param.slow_ema;
   m_signal_ema = m_param.signal_ema;
   if(!CheckParam(m_param))return(false);
   if(!InitIndicators())return(false);
   return(true);
}


bool cmodel_macd::CheckParam(cmodel_macd_param &m_param)
{
   if(!SymbolInfoInteger(m_symbol, SYMBOL_SELECT))
   {
      Print("Symbol ", m_symbol, " selection has failed. Check name of the symbol");
      return(false);
   }
   if(m_fast_ema == 0)
   {
      Print("Fast EMA must be greater than 0");
      return(false);
   }
   if(m_slow_ema == 0)
   {
      Print("Slow EMA must be greater than 0");
      return(false);
   }
   if(m_signal_ema == 0)
   {
      Print("Signal EMA must be greater than 0");
      return(false);
   }
   return(true);
}

bool cmodel_macd::InitIndicators()
{
   if(m_handle_macd==INVALID_HANDLE)
   {
      Print("Load indicators...");
      if((m_handle_macd=iMACD(m_symbol,m_timeframe,m_fast_ema,m_slow_ema,m_signal_ema,PRICE_CLOSE))==INVALID_HANDLE)
      {
         printf("Error creating MACD indicator");
         return(false);
      }
   }
   return(true);
}

bool cmodel_macd::Processing()
{
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   //if(m_account_info.TradeAllowed()==false)return(false);
   //if(m_account_info.TradeExpert()==false)return(false);
   
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main);
   m_macd_current=m_macd_buff_main[0];
   m_macd_previous=m_macd_buff_main[1];
   GetNumberOrders(m_orders);
   if(m_orders.buy_orders>0)   LongClosed();
   else                        LongOpened();
   if(m_orders.sell_orders!=0) ShortClosed();
   else                        ShortOpened();
   return(true);
}

bool cmodel_macd::LongOpened(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_SHORTONLY)return(false);
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false);
   
   bool rezult, ticket_bool;
   double lot=0.1;
   mm open_mm;
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main);
   
   m_macd_current=m_macd_buff_main[0];
   m_macd_previous=m_macd_buff_main[1];
   GetNumberOrders(m_orders);
   
   //Print("LongOpened");
   if(m_macd_current>0&&m_macd_previous<=0&&m_orders.buy_orders==0)
   {
      //lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), 0.0, m_delta);
      lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_BUY, m_symbol_info.Ask(), 0.1, 10000, m_delta);
      rezult=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_ADD, 0, lot, m_symbol_info.Ask(), 0, 0, "MACD Buy");
      return(rezult);
   }
   return(false);
}

bool cmodel_macd::ShortOpened(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_LONGONLY)return(false);
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false);
   
   bool rezult, ticket_bool;
   double lot=0.1;
   mm open_mm;
   
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main);
   
   m_macd_current=m_macd_buff_main[0];
   m_macd_previous=m_macd_buff_main[1];
   GetNumberOrders(m_orders);
   
   if(m_macd_current<=0&&m_macd_previous>=0&&m_orders.sell_orders==0)
   {
      //lot=open_mm.optimal_f(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), 0.0, m_delta);
      lot=open_mm.jons_fp(m_symbol, ORDER_TYPE_SELL, m_symbol_info.Bid(), 0.1, 10000, m_delta);
      rezult=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_ADD, 0, lot, m_symbol_info.Bid(), 0, 0, "MACD Sell");
      return(rezult);
   }
   return(false);
}

bool cmodel_macd::LongClosed(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   int rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==0)return(false);
   for(int i=total_elements-1;i>=0;i--)
   {
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)continue;
      if(t.Type()!=ORDER_TYPE_BUY)continue;
      m_symbol_info.Refresh();
      m_symbol_info.RefreshRates();
      CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main);
      if(m_symbol_info.Bid()<=t.StopLoss()&&t.StopLoss()!=0.0)
      {
         
         rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), 
                       m_symbol_info.Bid(), 0.0, 0.0, "MACD: buy close buy stop-loss");
      }
      if(m_macd_current<0&&m_macd_previous>=0)
      {
         //Print("Long position closed by Order Send");
         rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), 
                       m_symbol_info.Bid(), 0.0, 0.0, "MACD: buy close by signal");
      }
   }
   return(rez);
}

bool cmodel_macd::ShortClosed(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   int rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==0)return(false);
   for(int i=total_elements-1;i>=0;i--)
   {
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)continue;
      if(t.Type()!=ORDER_TYPE_SELL)continue;
      m_symbol_info.Refresh();
      m_symbol_info.RefreshRates();
      CopyBuffer(this.m_handle_macd,0,1,2,m_macd_buff_main);
      if(m_symbol_info.Ask()>=t.StopLoss()&&t.StopLoss()!=0.0)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
                                 m_symbol_info.Ask(), 0.0, 0.0, "MACD: sell close buy stop-loss");
      }
      if(m_macd_current>0&&m_macd_previous<=0)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
                                 m_symbol_info.Ask(), 0.0, 0.0, "MACD: sell close by signal");
      }
   }
   return(rez);
}

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

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

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

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

Настройки индикатора MACD будут тоже стандартными: fast EMA = 12, slow EMA = 26, signal MA = 9; Если требуется сконфигурировать модель определенным образом, такой инициализатор уже не подойдет. Нам потребуется инициализаторы с параметрами. Таких желательно (но не обязательно) сделать два типа. Первый будет получать свои параметры как классическая функция: Init(type param1, type param2, …, type paramN). Второй будет узнавать о параметрах модели с помощью специальной структуры, сохраняющей такие параметры. Этот вариант иногда бывает иногда предпочтительным, т.к. иногда количество параметров может быть большим, в этом случае было бы удобней передавать их через структуры.

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

Наша торговая модель имеет всего четыре торговые функции. Функция открытия длинной сделки: LongOpen, функция открытия короткой сделки: ShortOpen, функция закрытия длинной сделки: LongClosed, функция закрытия короткой сделки: ShortClosed. Работа функций LongOpen и ShortOpen тривиальна. Обе получают значения индикатора MACD предыдущего бара, которое сравнивают с значением два бара назад. Чтобы избежать эффекта "перерисовываемости", текущий (нулевой) бар было решено не использовать.

Если имеет место пересечение сверху вниз, то функция ShortOpen рассчитывает  с помощью функций включенных в заголовочный файл mm.mqh необходимый лот, после чего отправляет свой приказ функции OrderSend. LongClose в этот момент, наоборот, закрывает все длинные позиции модели. Это происходит так, функция последовательно перебирает все текущие открытые ордера в таблице ордеров модели. Если находится какой-либо длинный ордер, то функция закрывает его встречным ордером. Тоже,  но только наоборот, делает функция ShortClose(). Работу этих функций можно посмотреть в листинге, приведенном выше.

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

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

Рассмотрим такой случай: имеется ограничение на размер торговой позиции в 15 стандартных лотов в обоих направлениях. Текущая позиция длинная и равна 3 лотам. Торговая модель на основе своей системы управления капиталом хочет открыть длинную позицию объемом 18.6 лотов. Функция CheckLot() вернет скорректированное значение объема сделки. В данном случае оно будет равно 12 лотам (т.к. 3 лота из 15 возможных уже заняты другими сделками). Если бы текущая открытая  позиция была короткой, а не длинной, то функция вернула бы 15 лотов вместо 18.6. Это максимально возможный объем позиции.

После выставления 15 лотов buy, совокупная позиция в этом случае будет 12 лотов (3 – sell, 15 - buy). Когда другая модель перекроет свою первоначальную короткую позицию 3 лотами buy, общая позиция станет максимально возможной – 15 лотов. Другие сигналы на покупку не будут обрабатываться, пока модель не перекроет частично или полностью свои 15 лотов buy. Если возможный объем для запрашиваемой сделки исчерпан, функция вернет константу EMPTY_VALUE, такой сигнал нужно пропустить.

Если проверка возможности установленного объема прошла успешно, тогда делаются расчеты по величине требуемой маржи. Средств на счете может быть недостаточно для заявленного объема. Для этих целей существует функция CheckMargin(). Если маржи недостаточно, она попробует скорректировать заявленный объем сделки таким образом, чтобы текущая свободная маржа позволяла ее открыть. Если маржи недостаточно даже для открытия минимального объема, то мы находимся в состоянии Margin-Call.

Если в текущий момент нет никаких сделок, и маржа не используется, то это означает только одно – технический margin-call, состояние, когда невозможно открыть ни одной сделки. Без пополнения счета дальнейшее продолжение невозможно. Если какая-то маржа все-таки используется, то не остается ничего другого, как ждать, когда будет закрыта сделка, использующая эту маржу. В любом случае при недостатке маржи будет возвращена константа EMPTY_VALUE.

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

Суть первого метода заключается в определении какой-то фиксированной части счета, которой можно рисковать. Например, если допускается риск 2% от счета, а сам счет равен 10 000 долларов, то максимальный риск составит 200 долларов. Для того чтобы рассчитать, какой лот необходимо использовать для 200 долларового стопа, необходимо точно знать, какое максимальное  расстояние может проделать цена против открываемой позиции. Поэтому для расчета лота по этой формуле необходимо точно определить уровень Stop Loss и уровень цены, по которой будет совершена сделка.

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

Вот его решение:

x=((1.0+MathSqrt(1+4.0*d))/2)*Step;

где: x - нижняя граница перехода на следующий уровень, d = (Прибыль / Дельта) * 2.0, Step - шаг дельты, например 0.1 лот.

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

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

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

Для разминки создадим целых четыре модели. Пусть одна модель торгует на EURUSD с параметрами по умолчанию, второй тоже будет торговать на EURUSD, но на 15-минутном таймфрейме. Третья модель будет запущена на графике GBPUSD с параметрами по умолчанию. Четвертая – на USDCHF на двухчасовом графике с параметрами: Slow EMA=6, Fast EMA = 12, Signal EMA = 9. Период  для тестирования – H1, режим тестирования – все тики, время с 01.01.2010 по 01.09.2010.

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

Вот таблица, в которой приводятся основные показатели тестирования:

Система
Количество сделок
Прибыль, $
MACD(9,12,26)H1 EURUSD
123
1092
MACD (9,12,26) EURUSD M15
598
-696
MACD(9,6,12) USDCHF H2
153
-1150
MACD(9,12,26) GBPUSD H1
139
-282
Все системы
1013
-1032

Из таблицы следует, что общее количество сделок для всех моделей должно составлять 1 013, а общая прибыль должна быть -1032$.

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

Итак, вот итоговый тест:


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

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

Для этого обратимся к схеме, изображенной ниже:

Class reference

Схема показывает основные структуры экземпляра модели.

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

Затем при наступлении события OnTrade() или OnTick() последовательно опрашиваются все экземпляры моделей, находящихся в списке. Обращение к ним происходит последовательно через вызов функции Processing(). Далее она вызывает торговые функции своей модели (голубая группа функций). Их список и названия определены не жестко, однако удобно использовать унифицированные имена типа LongOpened(), ShortClosed() и т.д. Уже эти функции на основе заложенной в них логики выбирают момент для совершения сделки, после чего передают специально сформированный запрос на открытие или закрытие сделки функции SendOrder().

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


Торговая модель на основе индикатора полос Боллинджера

Теперь, когда общая структура данных и методов становится понятной, создадим еще одну торговую модель, основанную на трендовом индикаторе полос Боллинджера. За основу этой торговой модели был взят простой советник Боллинджер на стероидах Андрея Корнишкина. Полосы Боллинджера - это уровни, равные определенному размеру стандартных отклонений от простой скользящей средней. Более подробно о том, как устроен этот индикатор, можно почитать в справке по техническому анализу, прилагаемой к терминалу MetaTrader 5.

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

Мы немножко упростим торговую тактику и не станем использовать вспомогательный индикатор – двойную экспоненциальную скользящую среднюю (Double Exponential Moving Average или DEMA). Зато будем использовать жесткие защитные остановки - виртуальные Stop Loss. Они сделают процесс торговли более стабильным и в то же время помогут разобрать пример, в котором каждая торговая модель использует свои собственные независимые уровни защитных остановок.

В качестве уровней защитных остановок используется текущая цена плюс или минус значение индикатора волатильности ATR. Например, если текущее значение ATR равно 68 пунктам и наступает сигнал на продажу по цене 1.25720, то виртуальный Stop Loss для такой сделки будет равен 1.25720 + 0.0068 = 1,26400. То же, но наоборот, для покупки: 1.25720 - 0.0068 = 1,25040.

Исходный код этой модели представлен ниже:

#include <Models\Model.mqh>
#include <mm.mqh>
//+----------------------------------------------------------------------+
//| This model use Bollinger bands.
//| Buy when price is lower than lower band
//| Sell when price is higher than upper band
//+----------------------------------------------------------------------+  
struct cmodel_bollinger_param
{
   string            symbol;
   ENUM_TIMEFRAMES   timeframe;
   int               period_bollinger;
   double            deviation;
   int               shift_bands;
   int               period_ATR;
   double            k_ATR;
   double            delta;
};
   
class cmodel_bollinger : public CModel
{
private:
   int               m_bollinger_period;
   double            m_deviation;
   int               m_bands_shift;
   int               m_ATR_period;
   double            m_k_ATR;
   //------------Indicators Data:-------------
   int               m_bollinger_handle;
   int               m_ATR_handle;
   double            m_bollinger_buff_main[];
   double            m_ATR_buff_main[];
   //-----------------------------------------
   MqlRates          m_raters[];
   double            m_current_price;
public:
                     cmodel_bollinger();
   bool              Init();
   bool              Init(cmodel_bollinger_param &m_param);
   bool              Init(ulong magic, string name, string symbol, ENUM_TIMEFRAMES TimeFrame, double delta,
                          uint bollinger_period, double deviation, int bands_shift, uint ATR_period, double k_ATR);
   bool              Processing();
protected:
   bool              InitIndicators();
   bool              CheckParam(cmodel_bollinger_param &m_param);
   bool              LongOpened();
   bool              ShortOpened();
   bool              LongClosed();
   bool              ShortClosed();
   bool              CloseByStopSignal();
};

cmodel_bollinger::cmodel_bollinger()
{
   m_bollinger_handle   = INVALID_HANDLE;
   m_ATR_handle         = INVALID_HANDLE;
   ArraySetAsSeries(m_bollinger_buff_main,true);
   ArraySetAsSeries(m_ATR_buff_main,true);
   ArraySetAsSeries(m_raters, true);
   m_current_price=0.0;
}
//this default loader
bool cmodel_bollinger::Init()
{
   m_magic              = 322311;
   m_model_name         =  "Bollinger Bands Model";
   m_symbol             = _Symbol;
   m_timeframe          = _Period;
   m_bollinger_period   = 20;
   m_deviation          = 2.0;
   m_bands_shift        = 0;
   m_ATR_period         = 20;
   m_k_ATR              = 2.0;
   m_delta              = 0;
   if(!InitIndicators())return(false);
   return(true);
}

bool cmodel_bollinger::Init(cmodel_bollinger_param &m_param)
{
   m_magic              = 322311;
   m_model_name         = "Bollinger Model";
   m_symbol             = m_param.symbol;
   m_timeframe          = (ENUM_TIMEFRAMES)m_param.timeframe;
   m_bollinger_period   = m_param.period_bollinger;
   m_deviation          = m_param.deviation;
   m_bands_shift        = m_param.shift_bands;
   m_ATR_period        = m_param.period_ATR;
   m_k_ATR              = m_param.k_ATR;
   m_delta              = m_param.delta;
   //if(!CheckParam(m_param))return(false);
   if(!InitIndicators())return(false);
   return(true);
}

bool cmodel_bollinger::Init(ulong magic, string name, string symbol, ENUM_TIMEFRAMES timeframe, double delta,
                           uint bollinger_period, double deviation, int bands_shift, uint ATR_period, double k_ATR)
{
   m_magic           = magic;
   m_model_name      = name;
   m_symbol          = symbol;
   m_timeframe       = timeframe;
   m_delta           = delta;
   m_bollinger_period= bollinger_period;
   m_deviation       = deviation;
   m_bands_shift     = bands_shift;
   m_ATR_period      = ATR_period;
   m_k_ATR           = k_ATR;
   if(!InitIndicators())return(false);
   return(true);
}


/*bool cmodel_bollinger::CheckParam(cmodel_bollinger_param &m_param)
{
   if(!SymbolInfoInteger(m_symbol, SYMBOL_SELECT)){
      Print("Symbol ", m_symbol, " select failed. Check valid name symbol");
      return(false);
   }
   if(m_ma == 0){
      Print("Fast EMA must be bigest 0. Set MA = 12 (default)");
      m_ma=12;
   }
   return(true);
}*/

bool cmodel_bollinger::InitIndicators()
{
   m_bollinger_handle=iBands(m_symbol,m_timeframe,m_bollinger_period,m_bands_shift,m_deviation,PRICE_CLOSE);
   if(m_bollinger_handle==INVALID_HANDLE){
      Print("Error in creation of Bollinger indicator. Restart the Expert Advisor.");
      return(false);
   }
   m_ATR_handle=iATR(m_symbol,m_timeframe,m_ATR_period);
   if(m_ATR_handle==INVALID_HANDLE){
      Print("Error in creation of ATR indicator. Restart the Expert Advisor.");
      return(false);
   }
   return(true);
}

bool cmodel_bollinger::Processing()
{
   //if(timing(m_symbol,m_timeframe, m_timing)==false)return(false);
   
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   //if(m_account_info.TradeAllowed()==false)return(false);
   //if(m_account_info.TradeExpert()==false)return(false);
   
   //m_symbol_info.Name(m_symbol);
   //m_symbol_info.RefreshRates();
   //Copy last data of moving average
 
   GetNumberOrders(m_orders);
   
   if(m_orders.buy_orders>0)   LongClosed();
   else                        LongOpened();
   if(m_orders.sell_orders!=0) ShortClosed();
   else                        ShortOpened();
   if(m_orders.all_orders!=0)CloseByStopSignal();
   return(true);
}

bool cmodel_bollinger::LongOpened(void)
{
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_SHORTONLY)return(false);
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false);
   //Print("Model Bollinger: ", m_orders.buy_orders);
   bool rezult, time_buy=true;
   double lot=0.1;
   double sl=0.0;
   double tp=0.0;
   mm open_mm;
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   //lot=open_mm.optimal_f(m_symbol,OP_BUY,m_symbol_info.Ask(),sl,delta);
   CopyBuffer(m_bollinger_handle,2,0,3,m_bollinger_buff_main);
   CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main);
   CopyRates(m_symbol,m_timeframe,0,3,m_raters);
   if(m_raters[1].close>m_bollinger_buff_main[1]&&m_raters[1].open<m_bollinger_buff_main[1])
   {
      sl=NormalizeDouble(m_symbol_info.Ask()-m_ATR_buff_main[0]*m_k_ATR,_Digits);
      SendOrder(m_symbol,ORDER_TYPE_BUY,ORDER_ADD,0,lot,m_symbol_info.Ask(),sl,tp,"Add buy");
   }
   return(false);
}

bool cmodel_bollinger::ShortOpened(void)
{
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_LONGONLY)return(false);
   //if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_CLOSEONLY)return(false);
   
   bool rezult, time_sell=true;
   double lot=0.1;
   double sl=0.0;
   double tp;
   mm open_mm;
   
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   CopyBuffer(m_bollinger_handle,1,0,3,m_bollinger_buff_main);
   CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main);
   CopyRates(m_symbol,m_timeframe,0,3,m_raters);
   if(m_raters[1].close<m_bollinger_buff_main[1]&&m_raters[1].open>m_bollinger_buff_main[1])
   {   
      sl=NormalizeDouble(m_symbol_info.Bid()+m_ATR_buff_main[0]*m_k_ATR,_Digits);
      SendOrder(m_symbol,ORDER_TYPE_SELL,ORDER_ADD,0,lot,m_symbol_info.Ask(),sl,tp,"Add buy");
   }
   return(false);
}

bool cmodel_bollinger::LongClosed(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   int rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==0)return(false);
   m_symbol_info.Name(m_symbol);
   m_symbol_info.RefreshRates();
   CopyBuffer(m_bollinger_handle,1,0,3,m_bollinger_buff_main);
   CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main);
   CopyRates(m_symbol,m_timeframe,0,3,m_raters);
   if(m_raters[1].close<m_bollinger_buff_main[1]&&m_raters[1].open>m_bollinger_buff_main[1])
   {
      for(int i=total_elements-1;i>=0;i--)
      {
         if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
         t=ListTableOrders.GetNodeAtIndex(i);
         if(CheckPointer(t)==POINTER_INVALID)continue;
         if(t.Type()!=ORDER_TYPE_BUY)continue;
         m_symbol_info.Refresh();
         m_symbol_info.RefreshRates();
         rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(), 
                       m_symbol_info.Bid(), 0.0, 0.0, "BUY: close by signal");
      }
   }
   return(rez);
}

bool cmodel_bollinger::ShortClosed(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   int rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==0)return(false);
   CopyBuffer(m_bollinger_handle,2,0,3,m_bollinger_buff_main);
   CopyBuffer(m_ATR_handle,0,0,3,m_ATR_buff_main);
   CopyRates(m_symbol,m_timeframe,0,3,m_raters);
   if(m_raters[1].close>m_bollinger_buff_main[1]&&m_raters[1].open<m_bollinger_buff_main[1])
   {
      for(int i=total_elements-1;i>=0;i--)
      {
         if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
         t=ListTableOrders.GetNodeAtIndex(i);
         if(CheckPointer(t)==POINTER_INVALID)continue;
         if(t.Type()!=ORDER_TYPE_SELL)continue;
         m_symbol_info.Refresh();
         m_symbol_info.RefreshRates();
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
                       m_symbol_info.Ask(), 0.0, 0.0, "SELL: close by signal");
      }
   }
   return(rez);
}

bool cmodel_bollinger::CloseByStopSignal(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   bool rez=false;
   total_elements=ListTableOrders.Total();
   if(total_elements==0)return(false);
   for(int i=total_elements-1;i>=0;i--)
   {
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)continue;
      if(t.Type()!=ORDER_TYPE_SELL&&t.Type()!=ORDER_TYPE_BUY)continue;
      m_symbol_info.Refresh();
      m_symbol_info.RefreshRates();
      CopyRates(m_symbol,m_timeframe,0,3,m_raters);
      if(m_symbol_info.Bid()<=t.StopLoss()&&t.Type()==ORDER_TYPE_BUY)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_SELL, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
                       m_symbol_info.Bid(), 0.0, 0.0, "BUY: close by stop");
         continue;
      }
      if(m_symbol_info.Ask()>=t.StopLoss()&&t.Type()==ORDER_TYPE_SELL)
      {
         rez=SendOrder(m_symbol, ORDER_TYPE_BUY, ORDER_DELETE, t.Ticket(), t.VolumeInitial(),
                       m_symbol_info.Ask(), 0.0, 0.0, "SELL: close by stop");
         continue;
      }
   }
   return(rez);
}

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

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


Объединение торговых моделей инструментов и таймфреймов в единое целое

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

Оптимизация покажет, на каком рынке и таймфреймах модель наиболее эффективна. Для каждой модели будут выбраны только два лучших таймфрейма и три лучших инструмента. В итоге мы получим двенадцать независимых решений (2 модели * 3 инструмента * 2 таймфрейма), которые и протестируем все вместе. Конечно, выбранный способ оптимизации страдает так называемой "подгонкой" результатов, но для наших целей это не важно.

На графиках ниже представлены лучшие результаты выборки:

1.1 MACD EURUSD M30

1.1 MACD EURUSD M30

1.2 . MACD EURUSD H3


1.2 . MACD EURUSD H3

1.3 MACD AUDUSD H4

1.3 MACD AUDUSD H4

1.4 . MACD AUDUSD H1


1.4 . MACD AUDUSD H1

1.5 MACD GBPUSD H12


1.5 MACD GBPUSD H12

1.6 MACD GBPUSD H6


1.6 MACD GBPUSD H6

2.1 Bollinger GBPUSD M15


2.1 Bollinger GBPUSD M15

2.2 Bollinger GBPUSD H1


2.2 Bollinger GBPUSD H1

2.3 Bollinger EURUSD M30

2.3 Bollinger EURUSD M30

 

2.4  Bollinger EURUSD H4


2.4  Bollinger EURUSD H4

 

2.5 Bollinger USDCAD M15


2.5 Bollinger USDCAD M15

 

2.6 Bollinger USDCAD H2

2.6 Bollinger USDCAD H2

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

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

bool macd_default=true;
bool macd_best=false;
bool bollinger_default=false;
bool bollinger_best=false;

void InitModels()
{
   list_model = new CList;             // Инициализируем указатель списком моделей
   cmodel_macd *model_macd;            // Создаем указатель на модель MACD
   cmodel_bollinger *model_bollinger;  // Создаем указатель на модель Bollinger
   
//----------------------------------------MACD DEFAULT----------------------------------------
   if(macd_default==true&&macd_best==false)
   {
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      // Загрузка параметров завершилась успешно
      if(model_macd.Init(129475, "Model macd M15", _Symbol, _Period, 0.0, Fast_MA,Slow_MA,Signal_MA))
      { 
      
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
      {
                                 // Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
         " on symbol ", model_macd.Symbol(), " creation has failed");
      }
   }
//-------------------------------------------------------------------------------------------
//----------------------------------------MACD BEST------------------------------------------
   if(macd_best==true&&macd_default==false)
   {
      // 1.1 EURUSD H30; FMA=20; SMA=24; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd H30", "EURUSD", PERIOD_M30, 0.0, 20,24,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
               " on symbol ", model_macd.Symbol(), " created successfully");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
      {// Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
         " on symbol ", model_macd.Symbol(), " creation has failed");
      }
      // 1.2 EURUSD H3; FMA=8; SMA=12; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd H3", "EURUSD", PERIOD_H3, 0.0, 8,12,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
       {// Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
         " on symbol ", model_macd.Symbol(), " creation has failed");
      }
      // 1.3 AUDUSD H1; FMA=10; SMA=18; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd M15", "AUDUSD", PERIOD_H1, 0.0, 10,18,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
      {// Загрузка параметров завершилась неудачно                       
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
               " on symbol ", model_macd.Symbol(), " creation has failed");
      }
      // 1.4 AUDUSD H4; FMA=14; SMA=15; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd H4", "AUDUSD", PERIOD_H4, 0.0, 14,15,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
         " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else{// Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " creation has failed");
      }
      // 1.5 GBPUSD H6; FMA=20; SMA=33; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd H6", "GBPUSD", PERIOD_H6, 0.0, 20,33,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
      {// Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
               " on symbol ", model_macd.Symbol(), " creation has failed");
      }
      // 1.6 GBPUSD H12; FMA=12; SMA=30; 
      model_macd = new cmodel_macd; // Инициализиуруем указатель моделью MACD
      if(model_macd.Init(129475, "Model macd H6", "GBPUSD", PERIOD_H12, 0.0, 12,30,9))
      { 
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " successfully created");
         list_model.Add(model_macd);// Загружаем модель в список моделей
      }
      else
      {// Загрузка параметров завершилась неудачно
         Print("Print(Model ", model_macd.Name(), " with period = ", model_macd.Period(), 
              " on symbol ", model_macd.Symbol(), " creation has failed");
      }
   }
//----------------------------------------------------------------------------------------------
//-------------------------------------BOLLINGER DEFAULT----------------------------------------
   if(bollinger_default==true&&bollinger_best==false)
   {
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger",_Symbol,PERIOD_CURRENT,0,
                             period_bollinger,dev_bollinger,0,14,k_ATR))
      {
         Print("Model ", model_bollinger.Name(), " successfully created");
         list_model.Add(model_bollinger);
      }
   }
//----------------------------------------------------------------------------------------------
//--------------------------------------BOLLLINGER BEST-----------------------------------------
   if(bollinger_best==true&&bollinger_default==false)
   {
      //2.1 Symbol: EURUSD M30; period: 15; deviation: 2,75; k_ATR=2,75;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","EURUSD",PERIOD_M30,0,15,2.75,0,14,2.75))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
              ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
      //2.2 Symbol: EURUSD H4; period: 30; deviation: 2.0; k_ATR=2.25;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","EURUSD",PERIOD_H4,0,30,2.00,0,14,2.25))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
         ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
      //2.3 Symbol: GBPUSD M15; period: 18; deviation: 2.25; k_ATR=3.0;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","GBPUSD",PERIOD_M15,0,18,2.25,0,14,3.00))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
         ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
      //2.4 Symbol: GBPUSD H1; period: 27; deviation: 2.25; k_ATR=3.75;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","GBPUSD",PERIOD_H1,0,27,2.25,0,14,3.75))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
         ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
      //2.5 Symbol: USDCAD M15; period: 18; deviation: 2.5; k_ATR=2.00;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","USDCAD",PERIOD_M15,0,18,2.50,0,14,2.00))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
         ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
      //2.6 Symbol: USDCAD M15; period: 21; deviation: 2.5; k_ATR=3.25;
      model_bollinger = new cmodel_bollinger;
      if(model_bollinger.Init(1829374,"Bollinger","USDCAD",PERIOD_H2,0,21,2.50,0,14,3.25))
      {
         Print("Model ", model_bollinger.Name(), "Period: ", model_bollinger.Period(),
         ". Symbol: ", model_bollinger.Symbol(), " successfully created");
         list_model.Add(model_bollinger);
      }
   }
//----------------------------------------------------------------------------------------------
}

Теперь протестируем все двенадцать моделей одновременно:


Тест 12 торговых моделей


Капитализация полученных результатов

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

Так называемое оптимальное f – частный случай фиксировано-пропорционального метода. Суть этого метода в том, что на каждую сделку отводится лимит потерь, равный определенному проценту от счета. В консервативных стратегиях капитализации, как правило, применяется 2% лимит потерь. Т.е. при 10 000$ размер позиции рассчитывается таким образом, чтобы потеря после срабатывания Stop Loss не превышала 200$. Существует, однако, определенная функция роста итогового баланса от увеличения риска. Эта функция имеет колокообразный вид. Т.е. сначала, с ростом уровня процента риска растет итоговая прибыль. Однако есть пороговое значение риска на каждую сделку, после которого итоговый баланс начинает падать. Этот порог - и есть так называемое оптимальное f.

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

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

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

Ниже представлен получившийся график капитализации:

Тест с моделями капитализации

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

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


Работа с отложенными ордерами

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

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

Предложенная торговая модель позволяет работать с отложенными торговыми ордерами, хотя процесс контроля их выставления в этом случае значительно усложняется. Для работы с такими ордерами используется перегруженный метод Add(COrderInfo &order_info, double stop_loss, double take_profit), класса CTableOrders. В этом случае переменная m_type этого класса будет содержать соответствующий тип отложенного ордера, например ORDER_TYPE_BUY_STOP или ORDER_TYPE_SELL_LIMIT.

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

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

В код базовой модели входит специальный метод CModel::ReplaceDelayedOrders. Этот метод работает по следующему алгоритму. Сначала проверяются все активные ордера в таблице ордеров. Тикет каждого такого ордера сравнивается с тикетами ордеров размещенных в истории (HistoryOrderGetTicket()). Если тикет ордера в истории совпадет с отложенным ордером в таблице ордеров, однако статус ордера будет исполненным (ORDER_STATE_PARTIAL или ORDER_STATE_FILLED), то статус отложенного ордера в таблице ордеров также меняется на исполненный.

Далее, если с этим ордером не связаны никакие отложенные ордера имитирующие работу Stop Loss и Take Profit, такие ордера выставляются, а их тикеты заносятся в соответствующие значения таблицы (TicketSL, TicketTP). При этом отложенные ордера, имитирующие защитные остановки и уровни прибыли, выставляются по ценам, определенным заранее с помощью переменных m_sl и m_tp. Т.е. эти цены должны быть известны на момент вызова метода ReplaceDelayedOrders.

Примечательно, что метод приспособлен обрабатывать ситуации, когда выставляется ордер по рынку, по которому требуются отложенные ордера типов Stop Loss и Take Profit.

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

bool CModel::ReplaceDelayedOrders(void)
{
   if(m_symbol_info.TradeMode()==SYMBOL_TRADE_MODE_DISABLED)return(false);
   CTableOrders *t;
   int total_elements;
   int history_orders=HistoryOrdersTotal();
   ulong ticket;
   bool rez=false;
   long request;
   total_elements=ListTableOrders.Total();
   int try=0;
   if(total_elements==0)return(false);
   // Просматриваем каждый ордер в таблице
   for(int i=total_elements-1;i>=0;i--)
   {
      if(CheckPointer(ListTableOrders)==POINTER_INVALID)continue;
      t=ListTableOrders.GetNodeAtIndex(i);
      if(CheckPointer(t)==POINTER_INVALID)continue;
      switch(t.Type())
      {
         case ORDER_TYPE_BUY:
         case ORDER_TYPE_SELL:
            for(int b=0;i<history_orders;b++)
            {
               ticket=HistoryOrderGetTicket(b);
               // Если тикет исторического ордера равен одному из тикетов 
               // Stop Loss или Take Profit, значит ордер был перекрыт, и его нужно
               // удалить из таблицы ордеров
               if(ticket==t.TicketSL()||ticket==t.TicketTP())
               {
                  ListTableOrders.DeleteCurrent();
               }
            }
            // Если ордера имитирующие Stop Loss и Take Profit не найдены в истории,
            // возможно они еще не установлены. Следовательно необходимо их выставить,
            // задействуя процедуры для отложенных ордеров ниже:
            // цикл продолжается, выхода 'break' нет!!!
         case ORDER_TYPE_BUY_LIMIT:
         case ORDER_TYPE_BUY_STOP:
         case ORDER_TYPE_BUY_STOP_LIMIT:
         case ORDER_TYPE_SELL_LIMIT:
         case ORDER_TYPE_SELL_STOP:
         case ORDER_TYPE_SELL_STOP_LIMIT:
            for(int b=0;i<history_orders;b++)
            {
               ticket=HistoryOrderGetTicket(b);
               // Если тикет исторического ордера равен тикету отложенного ордера 
               // значит отложенный ордер сработал и необходимо выставить
               // отложенные ордера имитирующие работу Stop Loss и Take Profit.
               // Также необходимо перевести статус отложенного ордера в таблице
               // ордеров на исполненый (ORDER_TYPE_BUY или ORDER_TYPE_SELL)
               m_order_info.InfoInteger(ORDER_STATE,request);
               if(t.Ticket()==ticket&&
                  (request==ORDER_STATE_PARTIAL||request==ORDER_STATE_FILLED))
                  {
                  // Изменяем статус ордера в таблице ордеров:
                  m_order_info.InfoInteger(ORDER_TYPE,request);
                  if(t.Type()!=request)t.Type(request);
                  //------------------------------------------------------------------
                  // Выставляем отложенные ордера имитурующие Stop Loss и Take Profit:
                  // Уровень отложенных ордеров имитурующих Stop Loss и Take Profit
                  // должен быть определен за ранее. Также необходимо убедиться, что
                  // с текущим ордером уже не связан отложенный ордер имитирующий Stop Loss
                  // и Take Profit:
                  if(t.StopLoss()!=0.0&&t.TicketSL()==0)
                    {
                     // Пытаемся выставить отложенный ордер:
                     switch(t.Type())
                     {
                        case ORDER_TYPE_BUY:
                           // Делаем три попытки размещения отложенного ордера
                           for(try=0;try<3;try++)
                           {
                              m_trade.SellStop(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy");
                              if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
                              {
                                 t.TicketTP(m_trade.ResultDeal());
                                 break;
                              }
                           }
                        case ORDER_TYPE_SELL:
                           // Делаем три попытки размещения отложенного ордера
                           for(try=0;try<3;try++)
                           {
                              m_trade.BuyStop(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy");
                              if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
                              {
                                 t.TicketTP(m_trade.ResultDeal());
                                 break;
                              }
                           }
                     }
                  }
                  if(t.TakeProfit()!=0.0&&t.TicketTP()==0){
                     // Пытаемся выставить отложенный ордер имитирующий Take Profit:
                     switch(t.Type())
                     {
                        case ORDER_TYPE_BUY:
                           // Делаем три попытки размещения отложенного ордера
                           for(try=0;try<3;try++)
                           {
                              m_trade.SellLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy");
                              if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
                              {
                                 t.TicketTP(m_trade.ResultDeal());
                                 break;
                              }
                           }
                           break;
                        case ORDER_TYPE_SELL:
                           // Делаем три попытки размещения отложенного ордера
                           for(try=0;try<3;try++)
                           {
                              m_trade.BuyLimit(t.VolumeInitial(),t.StopLoss(),m_symbol,0.0,0.0,0,0,"take-profit for buy");
                              if(m_trade.ResultRetcode()==TRADE_RETCODE_PLACED||m_trade.ResultRetcode()==TRADE_RETCODE_DONE)
                              {
                                 t.TicketTP(m_trade.ResultDeal());
                                 break;
                              }
                           }
                     }
                  }
               }
            }
            break;
         
      }
   }
   return(true);
}

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


Заключение

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

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

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

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

Торговый движок самостоятельно определяет размеры максимальной позиции и максимальные объемы сделок. Специальный алгоритм дробит торговые запросы сразу на несколько независимых сделок, каждая из которых обслуживается в отдельности. Более того, представленная модель неплохо зарекомендовала себя в условиях соревнований Automated Trading Championship 2010 – все сделки проводятся четко и в соответствии с торговым планом, торговые модели, представленные на Чемпионате, управляются с разной степенью риска и по разным системам управления капиталом.

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