English 中文 Español Deutsch 日本語 Português
Обработка ошибок и логирование в MQL5

Обработка ошибок и логирование в MQL5

MetaTrader 5Примеры | 6 ноября 2015, 15:04
5 396 15
Sergey Eremin
Sergey Eremin

Введение

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

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


Основные методы обработки ошибок

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

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

Пример прекращения работы советника при помощи ExpertRemove()

void OnTick()
  {
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      Alert("fail");
      ExpertRemove();
      return;
     }
  }


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

Пример приведения некорректного значения к диапазону корректных

double VolumeCalculation()
  {
   double result=...;

   result=MathMax(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN));
   result=MathMin(result,SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX));

   return result;
  }

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


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

Пример возврата ошибочного значения

#define SOME_STR_FUNCTION_FAIL_RESULT (NULL)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      return SOME_STR_FUNCTION_FAIL_RESULT;
     }
   return result;
  }

void OnTick()
  {
   string someStr=SomeStrFunction();

   if(someStr==SOME_STR_FUNCTION_FAIL_RESULT)
     {
      Print("fail");
      return;
     }
  }

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


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

Пример присвоения кода ошибки при помощи SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE (123)

string SomeStrFunction()
  {
   string result="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE);
      return "";
     }
   return result;
  }

void OnTick()
  {
   ResetLastError();
   string someStr=SomeStrFunction();

   if(GetLastError()==ERR_USER_ERROR_FIRST+SOME_STR_FUNCTION_FAIL_CODE)
     {
      Print("fail");
      return;
     }
  }

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


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

Пример возвращения результата работы функции как bool

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail");
      return;
     }
  }

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

Пример возвращения результата работы функции как bool и присвоения кода ошибки при помощи SetUserError()

#define SOME_STR_FUNCTION_FAIL_CODE_1 (123)
#define SOME_STR_FUNCTION_FAIL_CODE_2 (124)

bool SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";

      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_1);

      return false;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      SetUserError(SOME_STR_FUNCTION_FAIL_CODE_2);
      return false;
     }
   value=resultValue;
   return true;
  }

void OnTick()
  {
   string someStr="";
   bool result=SomeStrFunction(someStr);

   if(!result)
     {
      Print("fail, code = "+(string)(GetLastError()-ERR_USER_ERROR_FIRST));
      return;
     }
  }

Однако такой вариант сложен для понимания (при чтении кода) и дальнейшей поддержки.


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

Пример возвращения результата работы функции как значения из перечисления (enum)

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

ENUM_SOME_STR_FUNCTION_RESULT SomeStrFunction(string &value)
  {
   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_1;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      value="";
      return SOME_STR_FUNCTION_FAIL_CODE_2;
     }

   value=resultValue;
   return SOME_STR_FUNCTION_SUCCES;
  }

void OnTick()
  {
   string someStr="";

   ENUM_SOME_STR_FUNCTION_RESULT result=SomeStrFunction(someStr);

   if(result!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result));
      return;
     }
  }

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


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

Пример возвращения результата работы функции как экземпляра структуры, состоящей из значения перечисления (enum) и результирующего значения

enum ENUM_SOME_STR_FUNCTION_RESULT
  {
   SOME_STR_FUNCTION_SUCCES,
   SOME_STR_FUNCTION_FAIL_CODE_1,
   SOME_STR_FUNCTION_FAIL_CODE_2
  };

struct SomeStrFunctionResult
  {
   ENUM_SOME_STR_FUNCTION_RESULT code;
   char              value[255];
  };

SomeStrFunctionResult SomeStrFunction()
  {
   SomeStrFunctionResult result;

   string resultValue="";
   bool resultOfSomeOperation=SomeOperation();

   if(!resultOfSomeOperation)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_1;
      return result;
     }

   bool resultOfSomeOperation2=SomeOperation2();

   if(!resultOfSomeOperation2)
     {
      result.code=SOME_STR_FUNCTION_FAIL_CODE_2;
      return result;
     }

   result.code=SOME_STR_FUNCTION_SUCCES;
   StringToCharArray(resultValue,result.value);
   return result;
  }

void OnTick()
  {
   SomeStrFunctionResult result=SomeStrFunction();

   if(result.code!=SOME_STR_FUNCTION_SUCCES)
     {
      Print("fail, error = "+EnumToString(result.code));
      return;
     }
   string someStr=CharArrayToString(result.value);
  }


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

Пример выполнения нескольких попыток открытия файла

string fileName="test.txt";
int fileHandle=INVALID_HANDLE;

for(int iTry=0; iTry<=10; iTry++)
  {
   fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);

   if(fileHandle!=INVALID_HANDLE)
     {
      break;
     }
   Sleep(iTry*200);
  }

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


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

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

input uint MAFastPeriod = 10;
input uint MASlowPeriod = 200;

int OnInit()
  {
//---
   if(MAFastPeriod>=MASlowPeriod)
     {
      Alert("Период быстрой скользящей средней должен быть меньше периода медленной скользящей средней!");
      return INIT_PARAMETERS_INCORRECT;
     }
//---
   return(INIT_SUCCEEDED);
  }

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


Общие рекомендации по обработке ошибок

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

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

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

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

  1. Разрешена ли торговля роботами на стороне терминала: TerminalInfoInteger(TERMINAL_TRADE_ALLOWED).
  2. Разрешена ли торговля роботами для данного счета: AccountInfoInteger(ACCOUNT_TRADE_EXPERT).
  3. Есть ли подключение к торговому серверу: TerminalInfoInteger(TERMINAL_CONNECTED).
  4. Корректны ли параметры торговой операции: OrderCheck().

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

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

Пример выполнения функции трейлинг стоп-лосс не чаще, чем раз в 30 секунд

const int TRAILING_STOP_LOSS_SECONDS_INTERVAL=30;

void TrailingStopLoss()
  {
   static datetime prevModificationTime=0;

   if((int)TimeCurrent() -(int)prevModificationTime<=TRAILING_STOP_LOSS_SECONDS_INTERVAL)
     {
      return;
     }

//--- модификация Stop Loss
     {
      ...
      ...
      ...
      prevModificationTime=TimeCurrent();
     }
  }

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


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

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


Выводите полезную информацию об ошибках. Старайтесь делать сообщения об ошибках достаточно информативными. Плохо, если программа выдает ошибку «не удалось открыть сделку» и ничего больше. Гораздо лучше, если сообщение будет в духе «не удалось открыть сделку: неверный объем открываемой позиции (0.9999)». Не важно, выводит программа информацию об ошибке во всплывающем окне или в лог-файл, она в любом случае должна быть достаточной, чтобы пользователь или программист (особенно при анализе лог-файла) смог понять причину ошибки и, если это возможно, исправить ее. При этом не следует и перегружать пользователя информацией: во всплывающем для пользователя окне можно не выдавать код ошибки, так как пользователю это мало что даст.


Логирование средствами MQL5

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


Уровни логирования

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

  • Debug: отладочные сообщения. Этот уровень логирования включается на этапах разработки, отладки и пуско-наладочных работ.
  • Info: информационные сообщения. Они несут в себе информацию о различных действиях системы (например: начало/окончание работы, открытие/закрытие сделок и т.п.). Обычно сообщения этого уровня не требуют никакой реакции, но могут ощутимо помочь при изучении цепочек событий, приведших к тем или иным ошибкам работы.
  • Warning: предупреждающие сообщения. Данный уровень сообщений может включать в себя описание ситуаций, приведших к ошибкам, которые не требуют вмешательства пользователя. К примеру, если рассчитанный объем сделки оказался меньше минимально допустимого, и программа автоматически его скорректировала, то об этом можно сообщить в лог-файл с уровнем «Warning».
  • Error: сообщения об ошибках, которые требуют явного вмешательства. Этот уровень логирования обычно применяется при возникновении ошибок вроде невозможности сохранения какого-то файла, открытия или модификации сделки и т.п. Иными словами, сюда попадают ошибки, которые программа не в состоянии самостоятельно преодолеть и которые требуют явного вмешательства (пользователя или программиста).
  • Fatal: сообщения о критических ошибках, которые приводят к невозможности дальнейшей работы программы. Такие сообщения требуют как можно более быстрой реакции, нередко для этого уровня предусматривается оповещение пользователя или программиста посредством электронной почты, SMS и т.п. Как будет показано далее, в MQL5 для оповещений можно применять PUSH-уведомления.


Ведение лог-файлов

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

Пример вывода сообщения в общий журнал советников при помощи функции Print()

double VolumeCalculation()
  {
   double result=...;
   if(result<SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN))
     {
      Print("Объем сделки (",DoubleToString(result,2),") оказался меньше допустимого и был скорректирован до "+DoubleToString(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN),2));
      result=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
     }
   return result;
  }

Данный подход обладает рядом недостатков:

  1. Сообщения от нескольких программ могут сваливаться в общую "кучу", что затруднит их анализ.
  2. Лог-файл, ввиду легкой доступности, может быть случайно или осознанно удален пользователем.
  3. Довольно трудно реализовать и настраивать уровни логирования.
  4. Невозможно перенаправлять вывод лог-сообщений в другой источник (внешний файл, база данных, почта и т.д.).
  5. Невозможно реализовать принудительную ротацию лог-файлов (смену файлов по дате и времени или по достижению некоторого размера).

Достоинства данного подхода:

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

Реализация собственного механизма логирования может устранить все недостатки применения Print() и PrintFormat(), однако при необходимости повторного использования кода потребует переноса в новый проект и механизма логирования (либо отказа от его использования в коде).

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

Пример реализации собственного механизма логирования на MQL5

//+------------------------------------------------------------------+
//|                                                       logger.mqh |
//|                                   Copyright 2015, Sergey Eryomin |
//|                                             http://www.ensed.org |
//+------------------------------------------------------------------+
#property copyright "Sergey Eryomin"
#property link      "http://www.ensed.org"

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")
//--- максимальное количество файлов для режима "новый лог-файл на каждый новый 1 Мб"
#define MAX_LOG_FILE_COUNTER (100000) 
//--- количество байт в мегабайте
#define BYTES_IN_MEGABYTE (1048576)
//--- максимальная длина имени лог-файла
#define MAX_LOG_FILE_NAME_LENGTH (255)
//--- уровни логирования
enum ENUM_LOG_LEVEL
  {
   LOG_LEVEL_DEBUG,
   LOG_LEVEL_INFO,
   LOG_LEVEL_WARNING,
   LOG_LEVEL_ERROR,
   LOG_LEVEL_FATAL
  };
//--- методы логирования
enum ENUM_LOGGING_METHOD
  {
   LOGGING_OUTPUT_METHOD_EXTERN_FILE,// внешний файл
   LOGGING_OUTPUT_METHOD_PRINT // функция Print
  };
//--- методы уведомлений
enum ENUM_NOTIFICATION_METHOD
  {
   NOTIFICATION_METHOD_NONE,// отключено
   NOTIFICATION_METHOD_ALERT,// функция Alert
   NOTIFICATION_METHOD_MAIL, // функция SendMail
   NOTIFICATION_METHOD_PUSH // функция SendNotification
  };
//--- типы ограничений лог-файлов
enum ENUM_LOG_FILE_LIMIT_TYPE
  {
   LOG_FILE_LIMIT_TYPE_ONE_DAY,// новый лог-файл на каждый новый день
   LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE // новый лог-файл на каждый новый 1 Мб
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CLogger
  {
public:
   //--- добавить сообщение в лог
   //--- Примечание:
   //--- если включен режим вывода во внешний файл, но сделать этот вывод не
   //--- удастся, то будет выполнен вывод сообщения через Print()
   static void Add(const ENUM_LOG_LEVEL level,const string message)
     {
      if(level>=m_logLevel)
        {
         Write(level,message);
        }

      if(level>=m_notifyLevel)
        {
         Notify(level,message);
        }
     }
   //--- задать уровни логирования
   static void SetLevels(const ENUM_LOG_LEVEL logLevel,const ENUM_LOG_LEVEL notifyLevel)
     {
      m_logLevel=logLevel;
      //--- уровень вывода сообщений через уведомления должен быть не ниже уровня записи сообщений в лог-файл
      m_notifyLevel=fmax(notifyLevel,m_logLevel);
     }
   //--- задать способ логирования
   static void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod)
     {
      m_loggingMethod=loggingMethod;
     }
   //--- задать способ уведомлений
   static void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod)
     {
      m_notificationMethod=notificationMethod;
     }
   //--- задать имя лог-файла
   static void SetLogFileName(const string logFileName)
     {
      m_logFileName=logFileName;
     }
   //--- задать тип ограничения на лог-файл
   static void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType)
     {
      m_logFileLimitType=logFileLimitType;
     }

private:
   //--- уровень логирования, сообщения не ниже которого будут сохраняться в лог-файл/журнал
   static ENUM_LOG_LEVEL m_logLevel;
   //--- уровень логирования, сообщения не ниже которого будут выводиться как уведомления
   static ENUM_LOG_LEVEL m_notifyLevel;
   //--- способ логирования
   static ENUM_LOGGING_METHOD m_loggingMethod;
   //--- способ уведомлений
   static ENUM_NOTIFICATION_METHOD m_notificationMethod;
   //--- имя лог-файла
   static string     m_logFileName;
   //--- тип ограничения на лог-файл
   static ENUM_LOG_FILE_LIMIT_TYPE m_logFileLimitType;
   //--- результат получения имени файла для лога           
   struct GettingFileLogNameResult
     {
                        GettingFileLogNameResult(void)
        {
         succes=false;
         ArrayInitialize(value,0);
        }
      bool              succes;
      char              value[MAX_LOG_FILE_NAME_LENGTH];
     };
   //--- результат проверки размера существующего лог-файла
   enum ENUM_LOG_FILE_SIZE_CHECKING_RESULT
     {
      IS_LOG_FILE_LESS_ONE_MEGABYTE,
      IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE,
      LOG_FILE_SIZE_CHECKING_ERROR
     };
   //--- выполнить запись в лог-файл
   static void Write(const ENUM_LOG_LEVEL level,const string message)
     {
      switch(m_loggingMethod)
        {
         case LOGGING_OUTPUT_METHOD_EXTERN_FILE:
           {
            GettingFileLogNameResult getLogFileNameResult=GetLogFileName();
            //---
            if(getLogFileNameResult.succes)
              {
               string fileName=CharArrayToString(getLogFileNameResult.value);
               //---
               if(WriteToFile(fileName,GetDebugLevelStr(level)+": "+message))
                 {
                  break;
                 }
              }
           }
         case LOGGING_OUTPUT_METHOD_PRINT:
            default:
              {
               Print(GetDebugLevelStr(level)+": "+message);
               break;
              }
        }
     }
   //--- выполнить уведомление
   static void Notify(const ENUM_LOG_LEVEL level,const string message)
     {
      if(m_notificationMethod==NOTIFICATION_METHOD_NONE)
        {
         return;
        }
      string fullMessage=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+message;
      //---
      switch(m_notificationMethod)
        {
         case NOTIFICATION_METHOD_MAIL:
           {
            if(TerminalInfoInteger(TERMINAL_EMAIL_ENABLED))
              {
               if(SendMail("Logger",fullMessage))
                 {
                  return;
                 }
              }
           }
         case NOTIFICATION_METHOD_PUSH:
           {
            if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED))
              {
               if(SendNotification(fullMessage))
                 {
                  return;
                 }
              }
           }
        }
      //---
      Alert(GetDebugLevelStr(level)+": "+message);
     }
   //--- получить имя лог-файла для записи
   static GettingFileLogNameResult GetLogFileName()
     {
      if(m_logFileName=="")
        {
         InitializeDefaultLogFileName();
        }
      //---
      switch(m_logFileLimitType)
        {
         case LOG_FILE_LIMIT_TYPE_ONE_DAY:
           {
            return GetLogFileNameOnOneDayLimit();
           }
         case LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE:
           {
            return GetLogFileNameOnOneMegabyteLimit();
           }
         default:
           {
            GettingFileLogNameResult failResult;
            failResult.succes=false;
            return failResult;
           }
        }
     }
   //--- получить имя лог-файла для случая ограничения "новый лог-файл на каждый новый день"
   static GettingFileLogNameResult GetLogFileNameOnOneDayLimit()
     {
      GettingFileLogNameResult result;
      string fileName=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+TimeToString(TimeLocal(),TIME_DATE);
      StringReplace(fileName,".","_");
      fileName=fileName+".log";
      result.succes=(StringToCharArray(fileName,result.value)==StringLen(fileName)+1);
      return result;
     }
   //--- получить имя лог-файла для случая ограничения "новый лог-файл на каждый новый 1 Мб"
   static GettingFileLogNameResult GetLogFileNameOnOneMegabyteLimit()
     {
      GettingFileLogNameResult result;
      //---
      for(int i=0; i<MAX_LOG_FILE_COUNTER; i++)
        {
         ResetLastError();
         string fileNameToCheck=m_logFileName+"_"+Symbol()+"_"+GetPeriodStr()+"_"+(string)i;
         StringReplace(fileNameToCheck,".","_");
         fileNameToCheck=fileNameToCheck+".log";
         ResetLastError();
         bool isExists=FileIsExist(fileNameToCheck);
         //---
         if(!isExists)
           {
            if(GetLastError()==5018)
              {
               continue;
              }
           }
         //---
         if(!isExists)
           {
            result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

            break;
           }
         else
           {
            ENUM_LOG_FILE_SIZE_CHECKING_RESULT checkLogFileSize=CheckLogFileSize(fileNameToCheck);

            if(checkLogFileSize==IS_LOG_FILE_LESS_ONE_MEGABYTE)
              {
               result.succes=(StringToCharArray(fileNameToCheck,result.value)==StringLen(fileNameToCheck)+1);

               break;
              }
            else if(checkLogFileSize!=IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE)
              {
               break;
              }
           }
        }
      //---
      return result;
     }
   //---
   static ENUM_LOG_FILE_SIZE_CHECKING_RESULT CheckLogFileSize(const string fileNameToCheck)
     {
      int fileHandle=FileOpen(fileNameToCheck,FILE_TXT|FILE_READ);
      //---
      if(fileHandle==INVALID_HANDLE)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      ResetLastError();
      ulong fileSize=FileSize(fileHandle);
      FileClose(fileHandle);
      //---
      if(GetLastError()!=0)
        {
         return LOG_FILE_SIZE_CHECKING_ERROR;
        }
      //---
      if(fileSize<BYTES_IN_MEGABYTE)
        {
         return IS_LOG_FILE_LESS_ONE_MEGABYTE;
        }
      else
        {
         return IS_LOG_FILE_NOT_LESS_ONE_MEGABYTE;
        }
     }
   //--- выполнить инициализацию имени лог-файла по умолчанию
   static void InitializeDefaultLogFileName()
     {
      m_logFileName=MQLInfoString(MQL_PROGRAM_NAME);
      //---
#ifdef __MQL4__
      StringReplace(m_logFileName,".ex4","");
#endif

#ifdef __MQL5__
      StringReplace(m_logFileName,".ex5","");
#endif
     }
   //--- записать сообщение в файл
   static bool WriteToFile(const string fileName,
                           const string text)
     {
      ResetLastError();
      string fullText=TimeToString(TimeLocal(),TIME_DATE|TIME_SECONDS)+", "+Symbol()+" ("+GetPeriodStr()+"), "+text;
      int fileHandle=FileOpen(fileName,FILE_TXT|FILE_READ|FILE_WRITE);
      bool result=true;
      //---
      if(fileHandle!=INVALID_HANDLE)
        {
         //--- попытаться поместить файловый указатель в конец файла            
         if(!FileSeek(fileHandle,0,SEEK_END))
           {
            Print("Logger: FileSeek() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
            result=false;
           }
         //--- попытаться записать текст в файл
         if(result)
           {
            if(FileWrite(fileHandle,fullText)==0)
              {
               Print("Logger: FileWrite() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
               result=false;
              }
           }
         //---
         FileClose(fileHandle);
        }
      else
        {
         Print("Logger: FileOpen() is failed, error #",GetLastError(),"; text = \"",fullText,"\"; fileName = \"",fileName,"\"");
         result=false;
        }
      //---
      return result;
     }
   //--- получить текущий период как строку
   static string GetPeriodStr()
     {
      ResetLastError();
      string periodStr=EnumToString(Period());
      if(GetLastError()!=0)
        {
         periodStr=(string)Period();
        }
      StringReplace(periodStr,"PERIOD_","");
      //---
      return periodStr;
     }
   //---
   static string GetDebugLevelStr(const ENUM_LOG_LEVEL level)
     {
      ResetLastError();
      string levelStr=EnumToString(level);
      //---
      if(GetLastError()!=0)
        {
         levelStr=(string)level;
        }
      StringReplace(levelStr,"LOG_LEVEL_","");
      //---
      return levelStr;
     }
  };
ENUM_LOG_LEVEL CLogger::m_logLevel=LOG_LEVEL_INFO;
ENUM_LOG_LEVEL CLogger::m_notifyLevel=LOG_LEVEL_FATAL;
ENUM_LOGGING_METHOD CLogger::m_loggingMethod=LOGGING_OUTPUT_METHOD_EXTERN_FILE;
ENUM_NOTIFICATION_METHOD CLogger::m_notificationMethod=NOTIFICATION_METHOD_ALERT;
string CLogger::m_logFileName="";
ENUM_LOG_FILE_LIMIT_TYPE CLogger::m_logFileLimitType=LOG_FILE_LIMIT_TYPE_ONE_DAY;
//+------------------------------------------------------------------+

Данный код можно поместить в отдельный включаемый файл, к примеру Logger.mqh, и сохранить в <каталог данных терминала>/MQL5/Include (этот файл приложен к статье). После этого работа с классом CLogger будет выглядеть примерно так:

Пример использования собственного механизма логирования

#include <Logger.mqh>

//--- выполнить инициализацию логгера
void InitLogger()
  {
//--- задать уровни логирования: 
//--- DEBUG-уровень для записи сообщений в лог-файл
//--- ERROR-уровень для уведомлений
   CLogger::SetLevels(LOG_LEVEL_DEBUG,LOG_LEVEL_ERROR);
//--- задать тип уведомлений как PUSH-уведомления
   CLogger::SetNotificationMethod(NOTIFICATION_METHOD_PUSH);
//--- задать способ логирования как запись во внешний файл
   CLogger::SetLoggingMethod(LOGGING_OUTPUT_METHOD_EXTERN_FILE);
//--- задать имя лог-файлов
   CLogger::SetLogFileName("my_log");
//--- задать тип ограничения на лог-файл как "новый лог-файл на каждый новый день"
   CLogger::SetLogFileLimitType(LOG_FILE_LIMIT_TYPE_ONE_DAY);
  }

int OnInit()
  {
//---
   InitLogger();
//---
   CLogger::Add(LOG_LEVEL_INFO,"");
   CLogger::Add(LOG_LEVEL_INFO,"---------- OnInit() -----------");
   LOG(LOG_LEVEL_DEBUG,"Example of debug message");
   LOG(LOG_LEVEL_INFO,"Example of info message");
   LOG(LOG_LEVEL_WARNING,"Example of warning message");
   LOG(LOG_LEVEL_ERROR,"Example of error message");
   LOG(LOG_LEVEL_FATAL,"Example of fatal message");
//---
   return(INIT_SUCCEEDED);
  }

Сначала в функции InitLogger() производится инициализация всех возможных параметров логгера, а затем выполняется запись сообщений в лог-файл. Результатом работы этого кода будет запись в лог-файл с именем вида «my_log_USDCAD_D1_2015_09_23.log» внутри <каталог_данных_терминала>/MQL5/Files следующего текста:

2015.09.23 09:02:10, USDCAD (D1), INFO: 
2015.09.23 09:02:10, USDCAD (D1), INFO: ---------- OnInit() -----------
2015.09.23 09:02:10, USDCAD (D1), DEBUG: Example of debug message (LoggerTest.mq5; int OnInit(); Line: 36)
2015.09.23 09:02:10, USDCAD (D1), INFO: Example of info message (LoggerTest.mq5; int OnInit(); Line: 38)
2015.09.23 09:02:10, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:02:10, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:02:10, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

Кроме того, через PUSH-уведомления будут отправлены сообщения уровней ERROR и FATAL.

Если задать уровень сообщений для записи в лог-файл как Warning (CLogger::SetLevels(LOG_LEVEL_WARNING,LOG_LEVEL_ERROR)), то вывод получится уже таким:

2015.09.23 09:34:00, USDCAD (D1), WARNING: Example of warning message (LoggerTest.mq5; int OnInit(); Line: 40)
2015.09.23 09:34:00, USDCAD (D1), ERROR: Example of error message (LoggerTest.mq5; int OnInit(); Line: 42)
2015.09.23 09:34:00, USDCAD (D1), FATAL: Example of fatal message (LoggerTest.mq5; int OnInit(); Line: 44)

То есть сохранение сообщений ниже уровня WARNING уже не будет выполняться.


Публичные методы класса CLogger и макрос LOG

Рассмотрим подробнее публичные методы класса Clogger и макрос LOG.


Метод void SetLevels(const ENUM_LOG_LEVEL logLevel, const ENUM_LOG_LEVEL notifyLevel). Задает уровни логирования.

const ENUM_LOG_LEVEL logLevel — уровень логирования, сообщения не ниже которого будут сохраняться в лог-файл/журнал. По умолчанию = LOG_LEVEL_INFO.

const ENUM_LOG_LEVEL notifyLevel — уровень логирования, сообщения не ниже которого будут выводиться как уведомления. По умолчанию = LOG_LEVEL_FATAL.

Возможные значения у обоих:

  • LOG_LEVEL_DEBUG,
  • LOG_LEVEL_INFO,
  • LOG_LEVEL_WARNING,
  • LOG_LEVEL_ERROR,
  • LOG_LEVEL_FATAL.


Метод void SetLoggingMethod(const ENUM_LOGGING_METHOD loggingMethod). Задает способ логирования.

const ENUM_LOGGING_METHOD loggingMethod — способ логирования. По умолчанию = LOGGING_OUTPUT_METHOD_EXTERN_FILE.

Возможные значения:

  • LOGGING_OUTPUT_METHOD_EXTERN_FILE — внешний файл,
  • LOGGING_OUTPUT_METHOD_PRINT — функция Print.


Метод void SetNotificationMethod(const ENUM_NOTIFICATION_METHOD notificationMethod). Задает способ уведомлений.

const ENUM_NOTIFICATION_METHOD notificationMethod — способ уведомлений. По умолчанию = NOTIFICATION_METHOD_ALERT.

Возможные значения:

  • NOTIFICATION_METHOD_NONE — отключено,
  • NOTIFICATION_METHOD_ALERT — функция Alert,
  • NOTIFICATION_METHOD_MAIL — функция SendMail,
  • NOTIFICATION_METHOD_PUSH — функция SendNotification.


Метод void SetLogFileName(const string logFileName). Задает имя лог-файла.

const string logFileName — имя лог файла. Значением по умолчанию будет выступать имя программы, в которой применяется логгер (см. приватный метод InitializeDefaultLogFileName()).


Метод void SetLogFileLimitType(const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType). Задает тип ограничения на лог-файл.

const ENUM_LOG_FILE_LIMIT_TYPE logFileLimitType - тип ограничения на лог-файл. Значение по умолчанию: LOG_FILE_LIMIT_TYPE_ONE_DAY.

Возможные значения:

  • LOG_FILE_LIMIT_TYPE_ONE_DAY — новый лог-файл на каждый новый день. Будут создаваться файлы с названиями my_log_USDCAD_D1_2015_09_21.log, my_log_USDCAD_D1_2015_09_22.log , my_log_USDCAD_D1_2015_09_23 .log и т.д.
  • LOG_FILE_LIMIT_TYPE_ONE_MEGABYTE — новый лог-файл на каждый новый 1 МБ. Будут создаваться файлы с названиями my_log_USDCAD_D1_0.log, my_log_USDCAD_D1_1.log, my_log_USDCAD_D1_2.log и т.д. Переход к следующему файлу по мере достижения предыдущим размера в 1 мегабайт.


Метод void Add(const ENUM_LOG_LEVEL level,const string message). Добавляет сообщение в лог.

const ENUM_LOG_LEVEL level — уровень сообщения. Возможные значения:

  • LOG_LEVEL_DEBUG
  • LOG_LEVEL_INFO
  • LOG_LEVEL_WARNING
  • LOG_LEVEL_ERROR
  • LOG_LEVEL_FATAL

const string message — текст сообщения.


Помимо метода Add, реализован также макрос LOG, который к тексту сообщения добавляет имя файла, сигнатуру функции и номер строки, откуда производится запись в лог-файл:

#define LOG(level, message) CLogger::Add(level, message+" ("+__FILE__+"; "+__FUNCSIG__+"; Line: "+(string)__LINE__+")")

Этот макрос может быть особенно полезен при отладке.

Таким образом, в примере показан механизм логирования, который позволяет:

  1. Настраивать уровни логирования (DEBUG..FATAL).
  2. Задавать, о сообщениях какого уровня нужно уведомлять пользователя.
  3. Задавать, куда писать лог — в журнал экспертов через Print() или во внешний файл.
  4. Для вывода во внешний файл — указывать имя файла и задавать ограничения на лог файлы: по файлу на каждую отдельную дату, или по файлу на каждый мегабайт лога.
  5. Задавать тип уведомлений (Alert(), SendMail(), SendNotify()).

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


Заключение

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

Прикрепленные файлы |
loggertest.mq5 (2.36 KB)
logger.mqh (12.52 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (15)
Sergey Eremin
Sergey Eremin | 10 нояб. 2015 в 09:00
Михаил:

Сергей!

Вот для таких ситуаций и существуют коды возврата торгового сервера.

Пример: Вы проверили наличие свободных средств и получили утвердительный результат.

Отсылаете ордер, а он не был принят (как Вы сказали : "Да мало ли что может произойти "),

так вот торговый сервер вернёт Вам в коде возврата ошибку "Недостаточно средств". 

Михаил, Вы правда думаете, что я это не понимаю? :)

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

1) Например файл: сначала проверили, существует ли такой файл, если нет, то создали и записали в него данные. После проверили, что получилось

2) Например граф. объект: сначала проверили, существует ли такой объект. Если нет, то создали, иначе подвинули (если координаты изменились). Потом проверили, что получилось.

и т.д. Почти что угодно, что зависит от чего-то другого и может завершиться неудачей.


Итак, давайте всё таки определимся/подытожим. Ваша основная претензия в том, что не рассмотрены коды возвратов ошибок торгового сервера и возможные реакции программы на них?

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

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

P.S. я уверен, что MQL4/5 это не только советники. Очевидно, что для Вас это в основном они. Но к примеру за последние два месяца я сделал раза в два больше индикаторов, чем советников. Так что всё очень субъективно.

Mikhail Filimonov
Mikhail Filimonov | 10 нояб. 2015 в 09:11

Сергей!

У меня нет претензий к Вашей статье.

Stanislav Korotky
Stanislav Korotky | 27 июл. 2018 в 20:50
Увы, очень неэффективный логгер. Хотел попользоваться, но видимо придется самому изобретать велосипед ;-).
Daniel Lagoshniak
Daniel Lagoshniak | 2 авг. 2021 в 22:05

по поводу эффективности логгера... На ЯП java тоже сначала сделали логгер, а потом долго мучались.

В java эту проблему решили с помощью var args, которые недоступны в MQL4/5. В чем проблема ? - сначала мы вызываем функции error.toString() потом конкатенируем все стринги в функции `CLogger::log(string1 + string2 + string3...); ` а лишь в конце проверяем - надо ли полученный стринг печатать.


Что бы я предложил взамен? - ничего нового, то что фактически стало стандартом в той же яве:

`if(CLogger::isLevel(LOG_LEVEL_INFO))   

     CLogger::log(string1 + string2 + string3);`

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

leonerd
leonerd | 3 мар. 2022 в 08:29

Всё завязано на один файл только. Что если надо вести два? Что если что-то надо лишь только принтить, а что-то писать в файл? Нет гибкости. Надо не через дефайн делать вызов, а через создание объекта.

Или например, хочу принтить и писать в файл. Или надо отправлять пуш и отправлять емэйл одновременно.

MQL5 для начинающих: Антивандальная защита графических объектов MQL5 для начинающих: Антивандальная защита графических объектов
Что должна делать ваша программа, если графические панели управления были удалены или изменены кем-то еще? В этой статье мы покажем, как после удаления приложения не иметь на графике "бесхозные" объекты, и как не потерять над ними контроль в случае переименования или удаления созданных программно объектов.
Оценка и выбор переменных для моделей машинного обучения Оценка и выбор переменных для моделей машинного обучения
В статье будут рассмотрены особенности выбора, предподготовки и оценки входных переменных (предикторов) для использования в моделях машинного обучения. Будут рассмотрены новые подходы и возможности по глубокому анализу предикторов, их влияние на возможное переобучение моделей. От результата этого этапа работы во многом зависит общий результат использования моделей. Будут рассмотрены два пакета, предлагающие новый и оригинальный подход к выбору предикторов.
Еще раз о картах Кохонена Еще раз о картах Кохонена
Cтатья описывает приемы работы с картами Кохонена. Она будет интересна как исследователям рынка с начальными навыками программирования на MQL4 и MQL5, так и опытным программистам, испытывающим сложности с подключением карт Кохонена к своим проектам.
Применение нечеткой логики в трейдинге средствами MQL4 Применение нечеткой логики в трейдинге средствами MQL4
В данной статье предлагаются примеры применения теории нечетких множеств в трейдинге средствами MQL4. Описывается разработка индикатора и советника с использованием библиотеки FuzzyNet для MQL4.