Как разработать надежный и безопасный торговый робот на языке MQL4

Shashev Sergei | 12 марта, 2007

Введение

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

  1. синтаксические – обнаруживаются на этапе компиляции программы и легко исправляются программистом;
  2. логические – компилятором не обнаруживаются. Путаница с именами переменных, неверный вызов функций, работа с данными разных типов и так далее;
  3. алгоритмические – возникают при неправильной расстановке скобок, в случае неразберихи с операторами ветвления и так далее;
  4. критические – крайне маловероятные ошибки, обычно нужно постараться, чтобы их вызвать. Тем не менее, при работе с dll бывают довольно часто;
  5. торговые – все типы ошибок, возникающих непосредственно при работе с ордерами. Такие ошибки являются бичем для торговых роботов.

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


Синтаксические ошибки

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

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


Логические, алгоритмические, критические ошибки

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

bool Some = false;
 
void check()
  {
    // Много кода
    Some = true;
  }
// Очень много кода
int start()
  {
    bool Some = false;
    //
    if(Some)   
      {
        //отсылка ордера
      }
   return(0);
  }

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

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

int profit = NormalizeDouble(SomeValue*point*2 / 3, digit);

мы пытаемся переменной типа int присвоить значение выражение типа double, в итоге получаем ноль. А ведь мы уровень тейкпрофита считаем! И такая ошибка в итоге ведет к неправильной торговле.

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

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

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

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

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


Торговые ошибки

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

Простейшая обработка вида:

ticket = OrderSend(Symbol(), OP_SELL, LotsOptimized(), Bid, 3,
         Bid + StopLoss*Point, Bid - TakeProfit*Point, 0, MAGICMA, 
         0, Red);
if(ticket > 0) 
  {
    err = GetLastError();
    Print("При открытии ордера возникла ошибка #", err);
  }

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

Вариант с бесконечным циклом:

while (true)
  {
    ticket = OrderSend(Symbol(), OP_SELL, Lots, Bid, slippage,
             Bid + StopLoss*Point, Bid - TakeProfit*Point, 0, 
             MAGICMA, 0, Red);
    if(ticket > 0) 
      {
        err = GetLastError();
        Print("При открытии ордера возникла ошибка #", err);
        break;      
      }
    Sleep(1000);
    RefleshRates();
  }

немного конечно помогает. Ордер пройдет на сервер с большей вероятностью. Но могут возникнуть проблемы:

  1. Брокеру не понравятся частые запросы;
  2. Ошибка фатальна, тогда запрос все равно не пройдет;
  3. Эксперт повиснет надолго;
  4. Сервер в принципе не принимает торговые запросы – выходные, праздник, профилактика и так далее.

Почти каждая ошибка уникальна и требует своей обработки. Поэтому рассмотрим вариант с оператором Switch и обработаем каждую ошибку более-менее индивидуально. Стандартная ошибка #146 - "Торговый поток занят", обработана с помощью семафора, реализованного в библиотеке TradeContext.mqh. Библиотеку и ее подробное описание можно взять в этой статье.

//Библиотека для разграничения работы с торговым потоком
//written by komposter
#include <TradeContext.mqh>
 
//параметры для сигналов
extern double MACDOpenLevel=3;
extern double MACDCloseLevel=2;
extern double MATrendPeriod=26;
 
// максимальное допустимое проскальзывание
int       slippage = 3;
//общее количество сделок
int deals = 0;
//время для отдыха после сделки
int TimeForSleep = 10;
//период запроса
int time_for_action = 1;
//количество попыток открытия/закрытия позиции
int count = 5;
//флаг работоспособности эксперта
bool Trade = true;
//флаг наличия денег для открытия позиции
bool NoOpen = false;
//+------------------------------------------------------------------+
//| В выходные не запрашиваем котировки у сервера                    |
//+------------------------------------------------------------------+
bool ServerWork()
  {     
   if(DayOfWeek() == 0 || DayOfWeek() == 6)
       return(false);
   return(true);      
  }
//+------------------------------------------------------------------+
//| Генерация magik                                                  |
//+------------------------------------------------------------------+ 
int GenericMagik()
  {
   return(deals);
  }
//+------------------------------------------------------------------+   
//| Закрытие сделок                                                  |
//+------------------------------------------------------------------+
bool CloseOrder(int magik)
  {
   int ticket,i;
   double Price_close;
   int err;
   int N;
//Функция пытается закрыть ордер за count попыток, если ей этого не удается
//то выдает сообщение об ошибке в журнал
   while(N < count)
     {          
       for(i = OrdersTotal() - 1; i >= 0; i--) 
         {
           if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
               if(OrderSymbol() == Symbol()) 
                   if(OrderMagicNumber() == magik)
                     {
                       if(OrderType() == OP_BUY)        
                           Price_close = NormalizeDouble(Bid, Digits);
                       if(OrderType() == OP_SELL)        
                           Price_close = NormalizeDouble(Ask, Digits);
                       if(OrderClose(OrderTicket(), OrderLots(),
                          Price_close,slippage))
                         { 
                           //уменьшаем количество сделок для этого эксперта
                           deals--;
                           //часть маржи освободилась - можно снова открываться
                           NoOpen = false;                           
                           return(true);
                         }
                         //дошли до этого места, значит ордер не отправился
                       N++;
                       //обработка возможных ошибок
                       err = ErrorBlock();
                       //если ошибка серьезная
                       if(err > 1)
                         {
                           Print("Требуется ручное закрытие ордера №",
                                 OrderTicket());
                           return(false);
                         }
                     }                                          
         }
        // отдыхаем 5 секунд и пытаемся снова закрыть сделку
       Sleep(5000);
       RefreshRates();
     }
    //если дошли до этого места, то сделка не закрылась за count попыток 
   Print("Требуется ручное закрытие ордера №",OrderTicket());
   return(false);
  }
//+------------------------------------------------------------------+
//|СделкаЮ для act 1-бай, 2-селл, второй параметр - к-во лотов       |
//+------------------------------------------------------------------+ 
int Deal(int act, double Lot)
  {
   int N = 0;
   int ticket;
   int err;
   double Price_open;
   double Lots;
   int cmd;
   int magik;
   magik = GenericMagik();
   Lots = NormalizeDouble(Lot,1);
   if(act == 1)
     {
       Price_open = NormalizeDouble(Ask, Digits);
       cmd = OP_BUY;
     }
   if(act == 2)
     {
       Price_open = NormalizeDouble(Bid, Digits);
       cmd = OP_SELL;
     }
   //проверка маржи для открытия позиции
   AccountFreeMarginCheck(Symbol(), cmd,Lots);
   err = GetLastError();
   if(err>0)
     {
       Print("No money for new position");
       NoOpen = true;
       return(0);
     }      
//Отсылаем ордер                  
   ticket = OrderSend(Symbol(), cmd, Lots, Price_open, slippage, 
                      0, 0, 0, magik);
   if(ticket > 0)
     {
       deals++;
       return(ticket);
     }
//Если не отослался, то пытаемся 5 раз снова его открыть      
   else
     {
       while(N < count)
         {
           N++;
           err = ErrorBlock();
           if(err == 1)
             {
               Sleep(5000);
               RefreshRates();
               if(act == 1)
                   Price_open = NormalizeDouble(Ask, Digits);
               if(act == 2)
                   Price_open = NormalizeDouble(Bid, Digits);              
               ticket = OrderSend(Symbol(), cmd, Lots, Price_open,
                                  slippage, 0, 0, 0, magik);
               if(ticket > 0)
                 {
                   deals++;
                   return(ticket);
                 }
             }
           // получили серьезную ошибку  
           if(err > 1)            
               return(0);               
         }                                                       
     }    
   return(0);
  }
//+------------------------------------------------------------------+
//| // 0-нет ошибки, 1-надо ждать и обновлять, 2-сделка бракуется,   |
//|    3-фатальная ошибка                                            |
//+------------------------------------------------------------------+
//Блок контроля ошибки 
int ErrorBlock()
  {
   int err = GetLastError();
   switch(err)
     {
       case 0: return(0);
       case 2:
         {
           Print("Сбой системы. Перезагрузить компьютер/проверить сервер");
           Trade = false;
           return(3);  
         }
       case 3:
         {
           Print("Ошибка в логике эксперта");
           Trade = false;
           return(3);   
         }
       case 4:
         {
           Print("Торговый сервер занят. Ждем 2 минуты");
           Sleep(120000);
           return(2);   
         }
       case 6:
         { 
           bool connect = false;
           int iteration = 0;
           Print("Disconnect ");
           while((!connect) || (iteration > 60))
             {
               Sleep(10000);
               Print("Связь не восстановлена, прошло ", iteration*10,
                     "  секунд");
               connect = IsConnected();
               if(connect)
                 {
                   Print("Связь восстановлена");
                   return(2);
                 }
               iteration++;
             }
           Trade = false; 
           Print("Проблемы с соединением");
           return(3);
         }
       case 8:
         {
           Print("Частые запросы");
           Trade = false; 
           return(3);
         }
       case 64:
         {
           Print("Счет заблокирован!");
           Trade = false; 
           return(3);            
         }
       case 65:
         {
           Print("Неправильный номер счета???");
           Trade = false; 
           return(3);            
         }
       case 128:
         {
           Print("Истек срок ожидания сделки");
           return(2);
         }
       case 129:
         {
           Print("Неверная цена");
           return(1);            
         }
       case 130:
         {
           Print("Неверный стоп");
           return(1);
         }
       case 131:
         {
           Print("Неверно рассчитывается объем сделки");
           Trade = false;            
           return(3);
         }
       case 132:
         {
           Print("Рынок закрыт");
           Trade = false; 
           return(2);
         }
       case 134:
         {
           Print("Не хватает маржи для проведения операции");
           Trade = false;             
           return(2);
         }
       case 135:
         {
           Print("Цены изменились");
           return (1);
         }
       case 136:
         {
           Print("Цен нет!");
           return(2);
         }
       case 138:
         {
           Print("Реквот, снова!");
           return(1);
         }
       case 139:
         {
           Print("Ордер в обработке. Глюк программы");
           return(2);
         }
       case 141:
         {
           Print("Слишком много запросов");
           Trade = false; 
           return(2);            
         }
       case 148:
         {
           Print("Слишком большой объем сделки");
           Trade = false; 
           return(2);            
         }                                          
     }
   return (0);
  }
//+------------------------------------------------------------------+
//| формирование сигналов на открытие/закрытие позиции по Macd       |
//+------------------------------------------------------------------+
int GetAction(int &action, double &lot, int &magik)
   {
   double MacdCurrent, MacdPrevious, SignalCurrent;
   double SignalPrevious, MaCurrent, MaPrevious;
   int cnt,total;
   
   MacdCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,0);
   MacdPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_MAIN,1);
   SignalCurrent=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,0);
   SignalPrevious=iMACD(NULL,0,12,26,9,PRICE_CLOSE,MODE_SIGNAL,1);
   MaCurrent=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,0);
   MaPrevious=iMA(NULL,0,MATrendPeriod,0,MODE_EMA,PRICE_CLOSE,1);
   
  if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious &&
         MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && MaCurrent>MaPrevious)
      {
         action=1;
         lot=1;
         return (0);
      }
  if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && 
         MacdCurrent>(MACDOpenLevel*Point) && MaCurrent<MaPrevious)
      {
         action=2;
         lot=1;
         return (0);               
      }
   total=OrdersTotal();
   for(cnt=0;cnt<total;cnt++)
     {
      OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES);
      if(OrderType()<=OP_SELL &&   // check for opened position 
         OrderSymbol()==Symbol())  // check for symbol
        {
         if(OrderType()==OP_BUY)   // long position is opened
           {
            // should it be closed?
            if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious &&
               MacdCurrent>(MACDCloseLevel*Point))
                {
                 action=3;
                 magik=OrderMagicNumber();
                 return(0); // exit
                }
           }
         else // go to short position
           {
            // should it be closed?
            if(MacdCurrent<0 && MacdCurrent>SignalCurrent &&
               MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point))
              {
               action=3;
               magik=OrderMagicNumber();
               return(0); 
              }
           }
        }
     }
   }
//+------------------------------------------------------------------+
//| expert initialization function                                   |
//+------------------------------------------------------------------+
int init()
  { 
    if(!IsTradeAllowed())
      {
        Print("Торговля не разрешена!");
        return(0);     
      }
  }
//+------------------------------------------------------------------+
//| expert deinitialization function                                 |
//+------------------------------------------------------------------+
int deinit()
  {
//Закрываем все ордера
   for(int k = OrdersTotal() - 1; k >= 0 ; k--)
       if(OrderSymbol() == Symbol()) 
         {
           if(OrderType() == OP_BUY)
              OrderClose(OrderTicket(), OrderLots(), 
                         NormalizeDouble(Bid,Digits), 10);               
           if(OrderType() == OP_SELL)
               OrderClose(OrderTicket(), OrderLots(),
                          NormalizeDouble(Ask, Digits),10);                 
         }
  } 
//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
  {
   int action =0;
   double lot = 1;
   int magik = 0;     
   while(Trade)
     {
       Sleep(time_for_action*1000);      
       RefreshRates();
       /*Логика эксперта, в которой вычисляем наше действие, размер позиции
       и magik для закрытия ордера
       action 1-buy, 2-sell, 3-close
       для примера возьмем эксперта на Macd*/
       GetAction(action,lot,magik);
       if(ServerWork())
         {
           if(((action == 1) || (action == 2)) && (!NoOpen))
             {                                        
               if(TradeIsBusy() < 0) 
                   return(-1); 
               Deal(action, lot);
               Sleep(TimeForSleep*1000);                                
               TradeIsNotBusy();
             }
           if(action == 3)
             {
               if(TradeIsBusy() < 0) 
                 { 
                   return(-1); 
                   if(!CloseOrder(magik))
                       Print("ТРЕБУЕТСЯ РУЧНОЕ ЗАКРЫТИЕ СДЕЛКИ");
                   Sleep(TimeForSleep*1000);   
                   TradeIsNotBusy();
                 } 
             }
         }
       else
         {
            Print("Выходные");
            if(TradeIsBusy() < 0) 
                  return(-1); 
            Sleep(1000*3600*48);
            TradeIsNotBusy();
         }
       action = 0;
       lot = 0;
       magik = 0;
     }
   Print("Возникла серьезная ошибка и эксперт остановил свою работу");  
   return(0);
  }
//+------------------------------------------------------------------+

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

  1. Получить сигнал от аналитического блока GetAction();
  2. Совершить необходимую транзакцию в функциях Deal() и CloseOrder();
  3. Возвратиться к пункту 1 после небольшого перерыва time_for_action при условии, что не было серьезных сбоев.

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

Ошибки обрабатываются в процедуре ErrorBlock() по следующей схеме: получается код ошибки и ему в соответствие приводится короткий алгоритм обработки. Для большинства - это просто вывод сообщения в журнал. Если ошибка серьезная, то происходит изменение флагов торговли Trade и NoOpen. Чуть сложнее обрабатывается ситуация со сбоем соединения. Там робот пытается достучаться до сервера с заданной периодичностью шестьдесят раз. Если не достучались, то скорее всего сервер имеет серьезные проблемы и следует на время прекратить всю торговлю. В зависимости от влияния ошибки на торговлю ее обрабатывающий алгоритм возвращает разные значения:


Заключение

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

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

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