Как упростить обнаружение и устранение ошибок в коде эксперта

Roman Kramar | 5 июля, 2007

Введение

Разработка торговых экспертов на языке MQL4 является непростой задачей сразу в нескольких аспектах:

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

Ошибки при использовании встроенных функций

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

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

1) Ошибка 130 - ERR_INVALID_STOPS
2) Ошибка 146 - ERR_TRADE_CONTEXT_BUSY

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

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

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

#include <stderror.mqh>
#include <stdlib.mqh>
 
void logError(string functionName, string msg, int errorCode = -1)
  {
    Print("ERROR: in " + functionName + "()");
    Print("ERROR: " + msg );
    
    int err = GetLastError();
    if(errorCode != -1) 
        err = errorCode;
        
    if(err != ERR_NO_ERROR) 
      {
        Print("ERROR: code=" + err + " - " + ErrorDescription( err ));
      }    
  }

В простейшем случае ее нужно использовать следующим образом:

void openLongTrade()
  {
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask + 5, 5, 0, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

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

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

 ERROR: in openLongTrade()
 ERROR: could not open order
 ERROR: code=138 - requote

То есть сразу будет видно:

  1. в какой функции произошла ошибка;
  2. к чему она относится (в данном случае - к попытке открыть позицию);
  3. какая именно ошибка возникла (код ошибки и ее описание).

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

void updateStopLoss(double newStopLoss)
  {
    bool modified = OrderModify(OrderTicket(), OrderOpenPrice(), 
                newStopLoss, OrderTakeProfit(), OrderExpiration());
    
    if(!modified)
      {
        int errorCode = GetLastError();
        if(errorCode != ERR_NO_RESULT ) 
            logError("updateStopLoss", "failed to modify order", errorCode);
      }
  }

Здесь в функции updateStopLoss() вызывается встроенная функция OrderModify(). Эта функция несколько отличается в плане обработки ошибок от OrderSend(). Если ни один из параметров изменяемого ордера не отличается от его текущих параметров, то функция вернет ошибку ERR_NO_RESULT. Если в нашем эксперте такая ситуация допустима, то мы должны игнорировать конкретно эту ошибку. Для этого мы анализируем значение возвращаемое GetLastError(). Если произошла ошибка с кодом ERR_NO_RESULT, то мы ничего не выводим в протокол.

Однако если произошла другая ошибка, то необходимо полностью отрапортовать о ней, как мы делали это раньше. Именно для этого мы сохраняем результат функции GetLastError() в промежуточной переменной и передаем его третьим параметром в функцию logError(). Дело в том, что встроенная функция GetLastError() автоматически обнуляет код последней ошибки после своего вызова. Если бы мы не передали код ошибки явно в logError(), то в протоколе была бы отражена ошибка с кодом 0 и описанием "no error".

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


Диагностика логических ошибок

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

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

Например, при протоколировании ошибок функций OrderSend(), OrderModify() и OrderClose() полезным бывает печатать в протокол текущее значение переменных Bid и Ask. Это несколько облегчает распознавание причин таких ошибок, как ERR_INVALID_STOPS и ERR_OFF_QUOTES.

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

void logInfo(string msg)
  {
    Print("INFO: " + msg);
  }

потому что:

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

void openLongTrade(double stopLoss)
  {
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask, 5, stopLoss, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

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

void openLongTrade( double stopLoss )
  {
    assert("openLongTrade", stopLoss < Bid, "stopLoss < Bid");
    
    int ticket = OrderSend(Symbol(), OP_BUY, 1.0, Ask, 5, stopLoss, 0);
    if(ticket == -1) 
        logError("openLongTrade", "could not open order");
  }

То есть мы фиксируем наше утверждение в коде при помощи новой вспомогательной функции assert(). Сама функция выглядит довольно просто:

void assert(string functionName, bool assertion, string description = "")
  {
    if(!assertion) 
        Print("ASSERT: in " + functionName + "() - " + description);
  }

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

  1. название функции, в которой условие было нарушено;
  2. описание нарушенного условия.

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

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

Еще одним полезным приемом является использование этой функции перед каждой операцией деления. Дело в том, что иногда в результате той или иной логической ошибки происходит деление на ноль. Работа эксперта в этом случае прекращается, а в протоколе появляется одна лишь строка с печальным диагнозом: 'zero divide'. Узнать же, в каком именно месте произошла эта коварная ошибка, если операция деления используется в коде неоднократно, достаточно сложно. Вот здесь и поможет нам функция assert(). Вставляем соответствующие проверки перед каждой операцией деления:

assert("buildChannel", distance > 0, "distance > 0");
double slope = delta / distance;

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



Анализ протокола работы эксперта на предмет наличия ошибок

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

Откройте Microsoft Excel и загрузите протокол работы эксперта как CSV-файл, указав, что в качестве разделителя нужно использовать один и более идущих подряд пробелов. Теперь включите "Автофильтр". В результате вы получите удобную возможность, взглянув на опции фильтра в двух соседние колонках (вы без труда поймете, в каких именно), понять были ли в протоколе ошибки, записанные туда терминалом или нет. Все записи, сформированные функциями logInfo(), logError() и assert(), начинаются с префикса ("INFO:", "ERROR:" и "ASSERT:"). Сообщения терминала об ошибках также будет легко увидеть, среди всего нескольких вариантов типовых сообщений о работе с ордерами.

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


Заключение

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