Создание мультивалютного мультисистемного советника

Maxim Khrolenko | 5 декабря, 2013

Введение

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

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

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


1. Схема эксперта

В общих чертах схема выглядит следующим образом:

Схема мультивалютно-мультистратегического советника

Рис. 1. Схема мультивалютно-мультистратегического советника

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

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

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

Стратегия А:

Стратегия В:

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

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

// Подключим стандартные библиотеки
// Создадим внешние параметры
// Создадим массивы, переменные, хэндлы индикаторов и т.д.

//--- Инициализация советника
int OnInit()
  {
   //--- Установим периодичность генерации события
   EventSetTimer(1); // 1 секунда
   // ...
   return(0);
  }
void OnTimer()
  {
   // ...
  }
//--- Деинициализация советника
void OnDeinit(const int reason)
  {
   //--- Остановка генерации события
   EventKillTimer();
   // ...
  }

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

Для доступа к свойствам счета, позиций, символов и торговым функциям будем использовать классы, соответственно CAccountInfo, CPositionInfo, CSymbolInfo и CTrade. Подключаем их к советнику:

//--- Подключим стандартные библиотеки
#include <Trade\AccountInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\Trade.mqh>

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

//--- Количество торгуемых символов для каждой стратегии
#define Strategy_A 2
#define Strategy_B 2

Затем создаем внешние параметры. С помощью констант определим размерность массивов, куда они будут скопированы. Создадим хэндлы индикаторов и другие глобальные переменные.

Ниже приведен пример для одного символа стратегии А:

//------------------- Внешние параметры стратегии А
input string          Data_for_Strategy_A="Strategy A -----------------------";
//--- Символ 0
input string          Symbol_A0      = "EURUSD";   // Символ
input bool            IsTrade_A0     = true;       // Разрешение на торговлю
//--- Параметры Полос Боллинджера (ВВ)
input ENUM_TIMEFRAMES Period_A0      = PERIOD_H1;  // Период ВВ
input uint            BBPeriod_A0    = 20;         // Период для расчета средней линии ВВ
input int             BBShift_A0     = 0;          // Смещение ВВ по горизонтали
input double          BBDeviation_A0 = 2.0;        // Кол-во стандартных отклонений ВВ
//...
//--- Общие параметры стратегии А
input double          DealOfFreeMargin_A = 1.0;    // Процент свободных средств для сделки
input uint            MagicNumber_A      = 555;    // Магик номер
input uint            Slippage_A         = 100;    // Допустимое проскальзывание для сделки
//...
//------------- Установим переменные стратегии А -----
//--- Массивы для внешних параметров
string          Symbol_A[Strategy_A];
bool            IsTrade_A[Strategy_A];
ENUM_TIMEFRAMES Period_A[Strategy_A];
int             BBPeriod_A[Strategy_A];
int             BBShift_A[Strategy_A];
double          BBDeviation_A[Strategy_A];
//--- Массивы для глобальных переменных
double          MinLot_A[Strategy_A],MaxLot_A[Strategy_A];
double          Point_A[Strategy_A],ContractSize_A[Strategy_A];
uint            DealNumber_A[Strategy_A];
datetime        Locked_bar_time_A[Strategy_A],time_arr_A[];
//--- Хэндлы индикаторов
int             BB_handle_high_A[Strategy_A];
int             BB_handle_low_A[Strategy_A];
//--- Массивы для значений индикаторов
double          BB_upper_band_high[],BB_lower_band_high[];
double          BB_upper_band_low[],BB_lower_band_low[];
//--- Класс
CTrade          Trade_A;
//...
//--- Установим глобальные переменные для всех стратегий
long            Leverage;
//--- Классы
CAccountInfo    AccountInfo;
CPositionInfo   PositionInfo;
CSymbolInfo     SymbolInfo;

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


2. Инициализация эксперта

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

//--- Получим размер кредитного плеча для счета
   Leverage=AccountInfo.Leverage();

Скопируем внешние переменные в массивы.

//--- Скопируем внешние параметры в массивы
   Symbol_A[0]     =Symbol_A0;
   IsTrade_A[0]    =IsTrade_A0;
   Period_A[0]     =Period_A0;
   BBPeriod_A[0]   =(int)BBPeriod_A0;
   BBShift_A[0]    =BBShift_A0;
   BBDeviation_A[0]=BBDeviation_A0;

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

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

Далее проверим, присутствует ли торгуемый символ в "Обзоре Рынка" и не был ли он использован более одного раза в рамках одной стратегии:

//--- Проверим наличие символа в "Обзоре Рынка"
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      if(IsSymbolInMarketWatch(Symbol_A[i])==false)
        {
         Print(Symbol_A[i]," не найден на сервере!");
         ExpertRemove();
        }
     }

//--- Проверим, используется ли символ более одного раза
   if(Strategy_A>1)
     {
      for(int i=0; i<Strategy_A-1; i++)
        {
         if(IsTrade_A[i]==false) continue;
         for(int j=i+1; j<Strategy_A; j++)
           {
            if(IsTrade_A[j]==false) continue;
            if(Symbol_A[i]==Symbol_A[j])
              {
               Print(Symbol_A[i]," используется более одного раза!");
               ExpertRemove();
              }
           }
        }
     }
//--- Функция IsSymbolInMarketWatch()
bool IsSymbolInMarketWatch(string f_Symbol)
  {
   for(int s=0; s<SymbolsTotal(false); s++)
     {
      if(f_Symbol==SymbolName(s,false))
         return(true);
     }
   return(false);
  }

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

Все это осуществим внутри цикла for.

//--- Общие действия
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      //--- Проверим ошибки во входных параметрах
      //...
      //--- Установим хэндлы для индикаторов
      BB_handle_high_A[i]=iBands(Symbol_A[i],Period_A[i],BBPeriod_A[i],BBShift_A[i],BBDeviation_A[i],
                                 PRICE_HIGH);
      if(BB_handle_high_A[i]<0)
        {
         Print("Хэндл Полос Боллинджера по ценам high для ",Symbol_A[i]," не создан. Хэндл=",INVALID_HANDLE,
               "\n Ошибка=",GetLastError());
         ExpertRemove();
        }
      //...
      //--- Рассчитаем данные для Лота
      //--- установим имя символа, для которого будет получена информация
      SymbolInfo.Name(Symbol_A[i]);
      //--- минимальный и максимальный размеры объема в торговых операциях
      MinLot_A[i]=SymbolInfo.LotsMin();
      MaxLot_A[i]=SymbolInfo.LotsMax();
      //--- размер пункта
      Point_A[i]=SymbolInfo.Point();
      //--- размер контракта
      ContractSize_A[i]=SymbolInfo.ContractSize();

      //--- Установим некоторые дополнительные параметры
     }

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

//--- Установим параметры для торговых операций
//--- установим магик номер
   Trade_A.SetExpertMagicNumber(MagicNumber_A);
//--- установим допустимое проскальзывание в пунктах при совершении сделки
   Trade_A.SetDeviationInPoints(Slippage_A);
//--- режим заполнения ордера, нужно использовать тот режим, который разрешается сервером
   Trade_A.SetTypeFilling(ORDER_FILLING_RETURN);
//--- режим логирования, лучше не вызывать этот метод, класс сам выставит оптимальный режим
   Trade_A.LogLevel(1);
//--- какую функцию использовать для торговли: true - OrderSendAsync(), false - OrderSend().
   Trade_A.SetAsyncMode(true);

Эту же процедуру повторяем для каждой стратегии, т.е.

  1. Копируем внешние переменные в массивы;
  2. Проверяем, правильно ли выбраны символы;
  3. Проверяем ошибки, устанавливаем хэндлы индикаторов, рассчитываем данные для лота и всего того, что необходимо для данной стратегии;
  4. Устанавливаем параметры для торговых операций.

Напоследок не помешает проверить, не был ли один и тот же символ выбран для нескольких стратегий (ниже приведен пример для двух стратегий):

//--- Проверим, используется ли один и тот же символ в нескольких стратегиях
   for(int i=0; i<Strategy_A; i++)
     {
      if(IsTrade_A[i]==false) continue;
      for(int j=0; j<Strategy_B; j++)
        {
         if(IsTrade_B[j]==false) continue;
         if(Symbol_A[i]==Symbol_B[j])
           {
            Print(Symbol_A[i]," используется в нескольких стратегиях!");
            ExpertRemove();
           }
        }
     }

3. Торговые циклы for

Так выглядит каркас из циклов for внутри функции OnTimer():

void OnTimer()
  {
//--- Проверим, подключен ли терминал к торговому серверу
   if(TerminalInfoInteger(TERMINAL_CONNECTED)==false) return;

//--- Секция A: Главный цикл оператора FOR для стратегии A -----------
   for(int A=0; A<Strategy_A; A++)
     {
      //--- A.1: Проверим, разрешена ли торговля по символу
      if(IsTrade_A[A]==false)
         continue; // прервать текущую итерацию FOR

     }

//--- Секция В: Главный цикл оператора FOR для стратегии В -----------
   for(int B=0; B<Strategy_B; B++)
     {
      //--- B.1: Проверим, разрешена ли торговля по символу
      if(IsTrade_B[B]==false)
         continue; // прервать текущую итерацию FOR

     }
  }

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

Если в мультистратегический советник надо будет перенести стратегию, где есть цикл for, внутри которого прописано условие прекращения всех последующих расчетов, то можно применить такую схему:

//--- Секция N: Главный цикл оператора FOR для стратегии N -----------
for(int N=0; N<Strategy_N; N++)
  {

   //...
   bool IsInterrupt=false;
   for(int i=0; i<Number; i++)
     {
      if(...) // прервать все расчеты
        {
         IsInterrupt=true;
         break;
        }
     }
   if(IsInterrupt=true)
      continue; // прервать текущую итерацию FOR
   //...

  }

После того как каркас из циклов for создан, просто переносим туда коды из других советников, а затем некоторые переменные заменяем на элементы массивов.

Например, предопределенную переменную _Symbol заменяем на Symbol_A[i] или _Point на Point_A[i]. Значения этих переменных характерны для данного символа, поэтому при инициализации они были скопированы в массивы.

Например, найдем значение индикатора:

 //--- A.3: Нижняя полоса BB расчитанная по ценам high
 if(CopyBuffer(BB_handle_high_A[A],LOWER_BAND,BBShift_A[A],1,BB_lower_band_high)<=0)
    continue; // прерываем текущую итерацию FOR
 ArraySetAsSeries(BB_lower_band_high,true);

Закрытие позиции на покупку запишем следующим способом:

 //--- A.7.1: Рассчитываем текущие цены Ask и Bid
 SymbolInfo.Name(Symbol_A[A]);
 SymbolInfo.RefreshRates();
 double Ask_price=SymbolInfo.Ask();
 double Bid_price=SymbolInfo.Bid();

 if(PositionSelect(Symbol_A[A]))
   {
    //--- A.7.2: Закрываем позицию BUY
    if(PositionInfo.PositionType()==POSITION_TYPE_BUY)
      {
       if(Bid_price>=BB_lower_band_high[0] || DealNumber_A[A]==0)
         {
          if(!Trade_A.PositionClose(Symbol_A[A]))
            {
             Print("Закрытие позиции Buy ",Symbol_A[A]," потерпело неудачу. Код=",Trade_A.ResultRetcode(),
                   " (",Trade_A.ResultRetcodeDescription(),")");
             continue; // прерываем текущую итерацию FOR
            }
          else
            {
             Print("Закрытие позиции Buy ",Symbol_A[A]," выполнено успешно. Код=",Trade_A.ResultRetcode(),
                   " (",Trade_A.ResultRetcodeDescription(),")");
             continue; // прерываем текущую итерацию FOR
            }
         }
      }

    //...
   }

Открытие позиции на покупку:

 //--- A.9.1: на Покупку
 if(Ask_price<=BB_lower_band_low[0])
   {
    //...

    //--- A.9.1.3: Совершаем сделку
    if(!Trade_A.Buy(OrderLot,Symbol_A[A]))
      {
       Print("Покупка ",Symbol_A[A]," потерпела неудачу. Код=",Trade_A.ResultRetcode(),
             " (",Trade_A.ResultRetcodeDescription(),")");
       continue; // прерываем текущую итерацию FOR
      }
    else
      {
       Print("Покупка ",Symbol_A[A]," выполнена успешно. Код=",Trade_A.ResultRetcode(),
             " (",Trade_A.ResultRetcodeDescription(),")");
       continue; // прерываем текущую итерацию FOR
      }
   }

При деинициализации не забудем остановить генерацию события таймера и удалить хэндлы индикаторов:

void OnDeinit(const int reason)
  {
//--- Остановка генерации события
   EventKillTimer();
//--- Удаляем хэндлы индикаторов
   for(int i=0; i<Strategy_A; i++)
     {
      IndicatorRelease(BB_handle_high_A[i]);
      IndicatorRelease(BB_handle_low_A[i]);
     }
  }


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

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

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


Ниже показаны настройки тестера:

Настройка тестера

Рис. 2. Настройка тестера стратегий

Результат для стратегии А, символ EURUSD:

Результат теста для стратегии A, символ EURUSD

Рис 3. Результат теста для стратегии A, символ EURUSD

Результат для стратегии А, символ GBPUSD:

Результат теста для стратегии A, символ GBPUSD

Рис 4. Результат теста для стратегии A, символ GBPUSD

Результат для стратегии B, символ AUDUSD:

Результат теста для стратегии В, символ AUDUSD

Рис. 5. Результат теста для стратегии В, символ AUDUSD

Результат для стратегии B, символ EURJPY:

Результат теста для стратегии В, символ EURJPY

Рис. 6. Результат теста для стратегии В, символ EURJPY

Результат теста по всем стратегиям и символам:

Результат теста по всем стратегиям и символам

Рис. 7. Результат теста по всем стратегиям и символам


Заключение

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

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