English
preview
MQL5 Bootstrap (II): Основные валидаторы для надежных торговых систем

MQL5 Bootstrap (II): Основные валидаторы для надежных торговых систем

MetaTrader 5 — Трейдинг |
30 0
Omega J Msigwa
Omega J Msigwa

Содержание


Введение

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

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

Типичные примеры:

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

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

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


Валидаторы размера лота

Чтобы избежать кода возврата торгового сервера 10014 ("Недопустимый объем в запросе"), убедитесь, что запрашиваемый объем находится в пределах минимального и максимального значений брокера и кратен шагу объема. Подробнее.

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

validators.mqh.

//+------------------------------------------------------------------+
//| Checks if a given lotsize (volume) value is appropriate according|
//| to instrument's specs.                                           |
//+------------------------------------------------------------------+
bool isValidLotsize(double volume, string symbol = NULL, bool verbose = false)
  {
   if(symbol == NULL || symbol == "")
      symbol = Symbol();

//--- minimal allowed volume for trade operations
   double min_volume = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   if(volume < min_volume)
     {
      if(verbose)
         printf("Volume is less than the minimal allowed SYMBOL_VOLUME_MIN=%.2f", min_volume);
      return(false);
     }

//--- maximal allowed volume of trade operations
   double max_volume = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX);
   if(volume > max_volume)
     {
      if(verbose)
         printf("Volume is greater than the maximal allowed SYMBOL_VOLUME_MAX=%.2f", max_volume);
      return(false);
     }

//--- get minimal step of volume changing
   double volume_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);

   int ratio = (int)MathRound(volume / volume_step);
   if(MathAbs(ratio * volume_step - volume) > 0.0000001)
     {
      if(verbose)
         printf("Volume is not a multiple of the minimal step SYMBOL_VOLUME_STEP=%.2f, the closest correct volume is %.2f", volume_step, ratio * volume_step);
      return(false);
     }

   return true;
  }

Функция возвращает true, если указанный объем удовлетворяет всем трем условиям.

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

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

//+------------------------------------------------------------------+
//|  Function to validate Lot size                                   |
//+------------------------------------------------------------------+
double NormalizeLotSize(const double volume, string symbol = "")
  {
   if(symbol == NULL || symbol == "")
      symbol = Symbol();

//--- Get the minimum, maximum, and step size for the symbol

   double min_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
   double max_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
   double step_volume = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

//--- Check if the volume is less than the minimum
   if(volume < min_volume)
      return min_volume;

//--- Check if the volume is greater than the maximum

   if(volume > max_volume)
      return max_volume;

//--- Check if the volume is a multiple of the step size

   int ratio = (int) MathRound(volume / step_volume);
   double adjusted_volume = ratio * step_volume;

   if(MathAbs(adjusted_volume - volume) > 0.0000001)
      return adjusted_volume;

   return adjusted_volume;
  }


Валидаторы стоп-лосса и тейк-профита

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

//+------------------------------------------------------------------+
//| Checks if either a given SL or TP is valid.                     |
//+------------------------------------------------------------------+
bool isValidStoploss_Takeprofit(ENUM_ORDER_TYPE type, double SL, double TP, string symbol = "", bool verbose = false)
  {

   if(symbol == NULL || symbol == "")
      symbol = Symbol();

//--- get the SYMBOL_TRADE_STOPS_LEVEL level

   int stops_level = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(stops_level != 0)
     {
      if(verbose)
         PrintFormat("SYMBOL_TRADE_STOPS_LEVEL=%d: StopLoss and TakeProfit must" +
                     " not be nearer than %d points from the closing price", stops_level, stops_level);
     }

//---

   MqlTick ticks;
   if(!SymbolInfoTick(symbol, ticks))
     {
      printf("Failed to obtain ticks from %s. Error = %d", symbol, GetLastError());
      return false;
     }

   double ask = ticks.ask, bid = ticks.bid;
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);

//---
   bool SL_check = false, TP_check = false;
//--- check only two order types
   switch(type)
     {
      //--- Buy operation
      case  ORDER_TYPE_BUY:
        {
         //--- check the StopLoss
         SL_check = (bid - SL > stops_level * point);
         if(!SL_check)
            if(verbose)
               PrintFormat("For order %s StopLoss=%.5f must be less than %.5f" +
                           " (bid=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                           EnumToString(type), SL, bid - stops_level * point, bid, stops_level);
         //--- check the TakeProfit
         TP_check = (TP - bid > stops_level * point);
         if(!TP_check)
            if(verbose)
               PrintFormat("For order %s TakeProfit=%.5f must be greater than %.5f" +
                           " (bid=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                           EnumToString(type), TP, bid + stops_level * point, bid, stops_level);
         //--- return the result of checking
         return(SL_check && TP_check);
        }
      //--- Sell operation
      case  ORDER_TYPE_SELL:
        {
         //--- check the StopLoss
         SL_check = (SL - ask > stops_level * point);
         if(!SL_check)
            if(verbose)
               PrintFormat("For order %s StopLoss=%.5f must be greater than %.5f " +
                           " (ask=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                           EnumToString(type), SL, ask + stops_level * point, ask, stops_level);
         //--- check the TakeProfit
         TP_check = (ask - TP > stops_level * point);
         if(!TP_check)
            if(verbose)
               PrintFormat("For order %s TakeProfit=%.5f must be less than %.5f " +
                           " (ask=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                           EnumToString(type), TP, ask - stops_level * point, ask, stops_level);
         //--- return the result of checking
         return(TP_check && SL_check);
        }
      break;
     }
//--- a slightly different function is required for pending orders
   return false;
  }

Поскольку уровни SL/TP могут определяться настройками риска, задаваться вручную или рассчитываться по индикаторам, их следующие значения нельзя надежно предсказать. Указанная выше функция не допускает отправки некорректных значений на сервер брокера, а аргумент verbose сообщает о таких случаях. При verbose=true функция выводит сведения о состоянии SL и TP в момент обнаружения недопустимого значения.


Нормализатор цены

Хотя сейчас он встречается реже, код возврата торгового сервера 10015 ("Неверная цена в запросе") остается одним из самых досадных кодов ошибок.

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

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

//+---------------------------------------------------------------------+
//| Normalizes the price of a particular symbol considering instrument's|
//| digits.                                                             |
//+---------------------------------------------------------------------+
double NormalizePrice(double price, string symbol = NULL)
  {
   if(symbol == NULL || symbol == "")
      symbol = Symbol();

   int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);

   return NormalizeDouble(price, digits);
  }


Проверка уровня заморозки для ордеров

Прежде чем изменять ордер, нужно убедиться, что новая цена входа не находится слишком близко к текущей цене Ask или Bid — в зависимости от типа ордера. Порог задается параметром SYMBOL_TRADE_FREEZE_LEVEL.

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

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

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

Тип ордера/позиции
Цена активации
Проверка
Ордер Buy Limit
Ask
Ask-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL
Ордер Buy Stop Ask OpenPrice-Ask >= SYMBOL_TRADE_FREEZE_LEVEL
Ордер Sell Limit Bid OpenPrice-Bid >= SYMBOL_TRADE_FREEZE_LEVEL
Ордер Sell Stop Bid Bid-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL
Позиция на покупку
Bid TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL
Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL
Позиция на продажу
Ask Ask-TakeProfit >= SYMBOL_TRADE_FREEZE_LEVEL
StopLoss-Ask >= SYMBOL_TRADE_FREEZE_LEVEL

Мы реализуем эту логику в функции OrderFreezeLevelCheck:

//+------------------------------------------------------------------+
//| Checks if the new order modification price isn't smaller than the|
//| freeze level value set by the broker.                            |
//+------------------------------------------------------------------+
bool OrderFreezeLevelCheck(ulong order_ticket, double new_price, bool verbose = true)
  {
//--- Get the order

   COrderInfo order;
   if(!order.Select(order_ticket))
     {
      printf("Failed to select an order with ticket %I64u. LastError = %d", order_ticket, GetLastError());
      return false;
     }

   string symbol = order.Symbol();

   MqlTick ticks;
   if(!SymbolInfoTick(symbol, ticks))
     {
      printf("Failed to obtain ticks from %s. Error = %d", symbol, GetLastError());
      return false;
     }

   double ask = ticks.ask, bid = ticks.bid;
   int freeze_level = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);

   ENUM_ORDER_TYPE order_type = order.OrderType();

//--- check the order type

   bool check = false;
   switch(order_type)
     {
      //--- BuyLimit pending order
      case  ORDER_TYPE_BUY_LIMIT:
        {
         //--- check the distance from the opening price to the activation price
         if(!(ask - new_price) > freeze_level * point)
           {
            if(verbose)
               PrintFormat("Order %s #%d cannot be modified: ask-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d points",
                           EnumToString(order_type), order_ticket, (int)((ask - new_price) / point), freeze_level);
            return false;
           }
        }
      break;
      case  ORDER_TYPE_SELL_LIMIT://--- BuyLimit pending order
        {
         //--- check the distance from the opening price to the activation price
         if(!(new_price - bid) > freeze_level * point)
           {
            if(verbose)
               PrintFormat("Order %s #%d cannot be modified: Open-bid=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d points",
                           EnumToString(order_type), order_ticket, (int)((new_price - bid) / point), freeze_level);

            return false;
           }
        }
      break;
      case  ORDER_TYPE_BUY_STOP: //--- BuyStop pending order
        {
         //--- check the distance from the opening price to the activation price
         if(!(new_price - ask) > freeze_level * point)
           {
            if(verbose)
               PrintFormat("Order %s #%d cannot be modified: ask-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d points",
                           EnumToString(order_type), order_ticket, (int)((new_price - ask) / point), freeze_level);

            return false;
           }
        }
      break;
      case  ORDER_TYPE_SELL_STOP: //--- SellStop pending order
        {
         //--- check the distance from the opening price to the activation price
         if(!(bid - new_price) > freeze_level * point)
           {
            if(verbose)
               PrintFormat("Order %s #%d cannot be modified: bid-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d points",
                           EnumToString(order_type), order_ticket, (int)((bid - new_price) / point), freeze_level);
            return false;
           }
        }
      break;
     }

   return true;
  }


Проверка достаточности средств для открытия сделки

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

//+------------------------------------------------------------------+
//| Checks if there is enough money to open a new trade.             |
//+------------------------------------------------------------------+
bool isEnoughMoneyForTrade(string symb, double lots, ENUM_ORDER_TYPE type, bool verbosity = false)
  {
//--- Getting the opening price

   MqlTick ticks;
   if(!SymbolInfoTick(symb, ticks))
      printf("Failed to get tick information on %s. Error = %d", symb, GetLastError());

   double price = ticks.ask;
   if(type == ORDER_TYPE_SELL)
      price = ticks.bid;

//--- values of the required and free margin

   double margin, free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);

//--- margin calculations
   if(!OrderCalcMargin(type, symb, lots, price, margin))
     {
      Print("Failed to calculate margin Error= ", GetLastError());
      return(false);
     }

//--- if there are insufficient funds to perform the operation

   if(margin > free_margin)
     {
      //--- report the error and return false
      if(verbosity)
         printf("Not enough money for %s(%s, %.3f). Error = %d", EnumToString(type), symb, lots, GetLastError());
      return(false);
     }

//--- checking successful
   return(true);
  }


Проверка наличия новых уровней при модификации

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

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

Для ордеров:

//+------------------------------------------------------------------+
//| Checking the new values of levels before order modification      |
//+------------------------------------------------------------------+
bool isOrderModificationSameLevels(ulong ticket, double new_price, double new_sl, double new_tp, bool verbosity = false)
  {
   COrderInfo order;

//--- select order by ticket
   if(order.Select(ticket))
     {
      //--- point size and name of the symbol, for which a pending order was placed
      string symbol = order.Symbol();
      double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
      int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);

      //--- check if there are changes in the Open price
      bool PriceOpenChanged = (MathAbs(order.PriceOpen() - new_price) > point);
      //--- check if there are changes in the StopLoss level
      bool StopLossChanged = (MathAbs(order.StopLoss() - new_sl) > point);
      //--- check if there are changes in the Takeprofit level
      bool TakeProfitChanged = (MathAbs(order.TakeProfit() - new_tp) > point);
      //--- if there are any changes in levels
      if(PriceOpenChanged || StopLossChanged || TakeProfitChanged)
         return(true);  // order can be modified

      //--- there are no changes in the Open, StopLoss and Takeprofit levels
      else
        {
         //--- notify about the error
         if(verbosity)
            PrintFormat("Order #%d already has levels of Open=%.5f SL=%.5f TP=%.5f", ticket, order.PriceOpen(), order.StopLoss(), order.TakeProfit());
        }
     }
//--- came to the end, no changes for the order
   return(false);       // no point in modifying
  }

Для позиций:

//+------------------------------------------------------------------+
//| Checking the new values of levels before order modification      |
//+------------------------------------------------------------------+
bool isPositionModificationSameLevels(ulong ticket, double new_sl, double new_tp, bool verbosity = false)
  {
   CPositionInfo pos;
//--- select order by ticket
   if(pos.SelectByTicket(ticket))
     {
      //--- point size and name of the symbol, for which a pending order was placed
      string symbol = pos.Symbol();
      double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
      //--- check if there are changes in the StopLoss level
      bool StopLossChanged = (MathAbs(pos.StopLoss() - new_sl) > point);
      //--- check if there are changes in the Takeprofit level
      bool TakeProfitChanged = (MathAbs(pos.TakeProfit() - new_tp) > point);
      //--- if there are any changes in levels
      if(StopLossChanged || TakeProfitChanged)
         return(true);  // position can be modified
      //--- there are no changes in the StopLoss and Takeprofit levels
      else
        {
         //--- notify about the error
         if(verbosity)
            PrintFormat("Position #%d already has levels of Open=%.5f SL=%.5f TP=%.5f", ticket, pos.PriceOpen(), pos.StopLoss(), pos.TakeProfit());
        }
     }
//--- came to the end, no changes for the order
   return(false);       // no point in modifying
  }

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

//+------------------------------------------------------------------+
//| Checks whether order's modification attempt is valid or not.     |
//+------------------------------------------------------------------+
bool OrderModificationCheck(ulong ticket, double new_price, double new_sl, double new_tp, bool verbosity = false)
  {
   if(!OrderFreezeLevelCheck(ticket, new_price, verbosity))
      return false;
   if(!isOrderModificationSameLevels(ticket, new_price, new_sl, new_tp, verbosity))
      return false;

   return true;
  }
//+------------------------------------------------------------------+
//| Checks whether position modification attempt is valid or not.    |
//+------------------------------------------------------------------+
bool PositionModificationCheck(ulong ticket, double new_sl, double new_tp, bool verbosity = false)
  {
   if(!isPositionModificationSameLevels(ticket, new_sl, new_tp, verbosity))
      return false;

   return true;
  }


Проверка, можно ли разместить новый ордер

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

//+------------------------------------------------------------------+
//| Check if another order can be placed                             |
//+------------------------------------------------------------------+
bool isNewOrderAllowed()
  {
//--- get the number of pending orders allowed on the account
   int max_allowed_orders = (int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);

//--- if there is no limitation, return true; you can send an order
   if(max_allowed_orders == 0)
      return(true);

//--- if we passed to this line, then there is a limitation; find out how many orders are already placed
   int orders = OrdersTotal();

//--- return the result of comparing
   return(orders < max_allowed_orders);
  }


Проверка появления нового бара

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

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

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

//+------------------------------------------------------------------+
//| Checks if a new bar has just emerged                             |
//+------------------------------------------------------------------+
bool isNewBar(ENUM_TIMEFRAMES tf)
  {
   int tf_seconds = PeriodSeconds(tf);
   int current_time_seconds = (int)TimeCurrent();

   return current_time_seconds % tf_seconds == 0;
  }


Проверка доступности символа для торговли

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

Символ может быть недоступен для торговли по ряду причин, в том числе:

Причина Пример
Символ используется только в информационных целях Индексы волатильности, индикаторы настроений рынка и собственные индексы брокера.
Рынок закрыт окончательно. Акция, исключенная из листинга; фьючерсный контракт с истекшим сроком действия.
Инвесторский счет/счет только для чтения На счете, доступном только для чтения, торговать нельзя.
Пользовательский символ Пользовательские символы, созданные локально, недоступны для торговли.

//+------------------------------------------------------------------+
//| Checks if the instrument is tradable                             |
//+------------------------------------------------------------------+
bool isSymbolTradable(string symbol = NULL)
  {
   if(symbol == NULL || symbol == "")
      symbol = _Symbol;

   long trade_mode = SymbolInfoInteger(symbol, SYMBOL_TRADE_MODE);
   return trade_mode != SYMBOL_TRADE_MODE_DISABLED;
  }


Проверка ближайших новостей

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

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

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

//+------------------------------------------------------------------+
//| Checks if there is a nearby NFP event                            |
//+------------------------------------------------------------------+
bool isNewsTime(string currency, ENUM_CALENDAR_EVENT_IMPORTANCE importance, uint period_minutes = 60)
  {
   if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
      return false;

//---

   long current_time_seconds = (long)TimeCurrent();
   long next_time_seconds = current_time_seconds + (period_minutes * 60);

//---

   static MqlCalendarValue values[]; //https://www.mql5.com/ru/docs/constants/structures/mqlcalendar#mqlcalendarvalue

   ResetLastError();
   int all_news = CalendarValueHistory(values, datetime(current_time_seconds), datetime(next_time_seconds), NULL, currency); //we obtain all the news with their values https://www.mql5.com/ru/docs/calendar/calendarvaluehistory

   if(all_news <= 0)  //if CalendarValue History returns a value less than zero it is and indicator that thre is either an error or there are no news
     {
      if(GetLastError() > 0) //we check if there was an error
         printf("Failed to get the news for %s. Error=%d", currency, GetLastError());
      else //if there was no error then there are no news for this symbol available since not all symbols have news
         return false;
     }

//---

   for(int i = 0; i < all_news; i++) //we loop through all the news
     {
      MqlCalendarEvent event;
      CalendarEventById(values[i].event_id, event); //Here among all the news we select one after the other by its id https://www.mql5.com/ru/docs/calendar/calendareventbyid

      MqlCalendarCountry country; //The couhtry where the currency pair originates
      CalendarCountryById(event.country_id, country); //https://www.mql5.com/ru/docs/calendar/calendarcountrybyid

      if(event.importance == importance) //filter the news by importance
        {
         if((long)MathAbs(current_time_seconds - values[i].time) <= (period_minutes * 60)) //filter the news by time | do not trade 15 minutes before or after the news | NB: the difference it time when subtracted gives out seconds
            return true; //There is a high impact new(s) coming shortly
        }
     }

   return false;
  }

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

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

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

Например, если мы хотим избежать открытия сделок в течение часа до и после выхода новостей высокой важности по USD, можно написать:

if(IsNewsTime("USD", CALENDAR_IMPORTANCE_HIGH, 60))
{
   Print("High-impact USD news detected. Trading suspended.");
   return;
}


Функции определения торговых сессий

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

Торговые сессии Форекс (UTC):

Торговая сессия Форекс Открытие (UTC) Закрытие (UTC)
Сидней 22:00  07:00
Токио 00:00  09:00
Лондон 08:00  16:00
Нью-Йорк 13:00  22:00
//+------------------------------------------------------------------+
//| Trading Sessions                                                 |
//+------------------------------------------------------------------+
enum ENUM_TRADING_SESSION
{
   SESSION_SYDNEY,
   SESSION_TOKYO,
   SESSION_LONDON,
   SESSION_NEWYORK,
   SESSION_UNKNOWN
};
//+------------------------------------------------------------------+
//| Returns the trading session using UTC/GMT time                   |
//+------------------------------------------------------------------+
ENUM_TRADING_SESSION GetTradingSession(datetime utc_time=0)
{
   if(utc_time == 0)
      utc_time = TimeGMT();

   MqlDateTime dt;
   TimeToStruct(utc_time, dt);

   int hour = dt.hour;

//--- Sydney
   if(hour >= 22 || hour < 7)
      return SESSION_SYDNEY;

//--- Tokyo
   if(hour >= 0 && hour < 9)
      return SESSION_TOKYO;

//--- London
   if(hour >= 8 && hour < 17)
      return SESSION_LONDON;

//--- New York
   if(hour >= 13 && hour < 22)
      return SESSION_NEWYORK;
      
   return SESSION_UNKNOWN;
}

Функция GetTradingSession() возвращает значение текущей сессии. С помощью приведенной выше функции мы можем создать отдельные вспомогательные функции для определения каждой торговой сессии.

//+------------------------------------------------------------------+
//|  Checks if the current session in the market is Sydney           |
//+------------------------------------------------------------------+
bool isSydneySession(datetime utc_time=0)
 {
   return GetTradingSession(utc_time) == SESSION_SYDNEY;
 }
//+------------------------------------------------------------------+
//|  Checks if the current session in the market is Tokyo            |
//+------------------------------------------------------------------+
bool isTokyoSession(datetime utc_time=0)
 {
   return GetTradingSession(utc_time) == SESSION_TOKYO;
 }
//+------------------------------------------------------------------+
//|  Checks if the current session in the market is London           |
//+------------------------------------------------------------------+
bool isLondonSession(datetime utc_time=0)
 {
   return GetTradingSession(utc_time) == SESSION_LONDON;
 }
//+------------------------------------------------------------------+
//|  Checks if the current session in the market is New York         |
//+------------------------------------------------------------------+
bool isNewYorkSession(datetime utc_time=0)
 {
   return GetTradingSession(utc_time) == SESSION_NEWYORK;
 }


Связываем все воедино

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

Стратегия проста. Если отложенных ордеров нет, советник размещает одновременно ордер Buy Stop и ордер Sell Stop. Ордер Buy Stop размещается выше текущей цены Ask, а ордер Sell Stop — ниже текущей цены Bid. Расстояние каждого отложенного ордера от текущей рыночной цены определяется индикатором Average True Range (ATR), что позволяет стратегии автоматически адаптироваться к изменениям рыночной волатильности.

   if (isNewOrderAllowed() && desired_trading_sessions)
    {
      uint buy_positions = PositionCount(user_symbol, magic_number, POSITION_TYPE_BUY),
           sell_positions = PositionCount(user_symbol, magic_number, POSITION_TYPE_SELL);
      
      if (!OrderExists(user_symbol, magic_number, ORDER_TYPE_BUY_STOP))
        {
          double sl = upper_entry-atr_value*sl_multiplier,
                 tp = upper_entry+atr_value*tp_multiplier;
          
          LOTS = NormalizeLotSize(lotsize, user_symbol); 
          
          if (isEnoughMoneyForTrade(user_symbol, LOTS, ORDER_TYPE_BUY_STOP, true) && buy_positions<max_open_positions)
              {
                m_trade.BuyStop(LOTS, upper_entry, user_symbol, sl, tp);       
              }
        }
      
   //---
   
      if (!OrderExists(user_symbol, magic_number, ORDER_TYPE_SELL_STOP))
        {
          double sl = lower_entry+atr_value*sl_multiplier,
                 tp = lower_entry-atr_value*tp_multiplier;
          
          LOTS = NormalizeLotSize(lotsize, user_symbol);
          if (isEnoughMoneyForTrade(user_symbol, LOTS, ORDER_TYPE_SELL_STOP, true) && sell_positions<max_open_positions)
             {
               m_trade.SellStop(LOTS, lower_entry, user_symbol, sl, tp);       
             }
     }

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

//--- Since the Market can move away from a pending order, we modify it constanly relative to the atr value,
//--- bringing it closer to the market every time the market moves away from it.

   if(isNewBar(Period()))  //During a new bar
     {
      for(int i = OrdersTotal() - 1; i >= 0; i--)
        {
         if(m_order.SelectByIndex(i))
            if(m_order.Magic() == magic_number && m_order.Symbol() == user_symbol)
              {
               ulong ticket = m_order.Ticket();

               switch(m_order.OrderType())
                 {
                  case  ORDER_TYPE_BUY_STOP:
                     if(ask < upper_entry)
                       {
                        double new_sl = upper_entry - atr_value * sl_multiplier, new_tp = upper_entry + atr_value * tp_multiplier;

                        if(OrderModificationCheck(ticket, upper_entry, new_sl, new_tp))
                           m_trade.OrderModify(ticket, upper_entry, new_sl, new_tp, ORDER_TIME_GTC, 0);
                       }
                     break;
                  case  ORDER_TYPE_SELL_STOP:
                     if(bid > lower_entry)
                       {
                        double new_sl = lower_entry + atr_value * sl_multiplier, new_tp = lower_entry - atr_value * tp_multiplier;

                        if(OrderModificationCheck(ticket, lower_entry, new_sl, new_tp))
                           m_trade.OrderModify(ticket, lower_entry, new_sl, new_tp, ORDER_TIME_GTC, 0);
                       }
                     break;
                  default:
                     printf("Unsupported order type");
                     break;
                 }
              }
        }
     }

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

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

   datetime utc_time = MQLInfoInteger(MQL_TESTER) ? TimeCurrent() : TimeGMT();
   bool desired_trading_sessions = (isNewYorkSession(utc_time) || isLondonSession(utc_time));
   
   if (nearby_news_found)
     {
       CancelOrders(user_symbol, magic_number);
       PositionClose(slippage, user_symbol, magic_number);
       return; //Halt trading operations afterwards
     }
     
//---
   
   if (isNewOrderAllowed() && desired_trading_sessions)
     {
        // Trading logic

Всего несколькими строчками кода мы можем реализовать надежную торговую стратегию.

Ниже показаны процесс тестирования на истории и результаты тестера за период с июля 2025 года по февраль 2026 года.

График тестера:

Отчет:


Заключение

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

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

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

Следите за обновлениями!


Таблица вложений

Имя файла Описание и использование
MQL5\Experts\Bootstrap\Test EA.mq5 Пример советника, демонстрирующий использование переиспользуемых библиотек Bootstrap.
MQL5\Include\Bootstrap\orders.mqh Вспомогательная библиотека с переиспользуемыми функциями для работы с отложенными ордерами: создание, изменение, получение данных и т.д.
MQL5\Include\Bootstrap\positions.mqh Вспомогательная библиотека с переиспользуемыми функциями для работы с открытыми позициями: поиск, подсчет, закрытие и т.д.
MQL5\Include\Bootstrap\validators.mqh Набор переиспользуемых функций валидации и вспомогательных функций, представленных в этой статье.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23204

Прикрепленные файлы |
MQL5.zip (10.8 KB)
За пределами GARCH (Часть VIII): Библиотека MMAR и ее использование в советнике За пределами GARCH (Часть VIII): Библиотека MMAR и ее использование в советнике
Эта статья завершает проект MMAR фасадным классом CMMAR и демонстрационным советником для MetaTrader 5. Фасад предоставляет компактный API (настройка, Fit(), Forecast()), объединяющий анализ разбиений, подгонку спектра и моделирование методом Монте-Карло. Вы узнаете, как загружать данные, подгонять модель и получать прогноз волатильности, а также использовать диагностику и обработку статусов для надежного применения модели в советниках.
Персистентная гомология в MQL5: Алгоритм приведения и диаграмма персистентности Персистентная гомология в MQL5: Алгоритм приведения и диаграмма персистентности
Мы завершаем реализацию персистентной гомологии в MQL5, приводя граничную матрицу Вьеториса-Рипса к диаграмме персистентности. В статье реализованы столбцовое приведение над Z/2 (CTDAReduction), контейнер диаграммы с аналитическими методами (CTDADiagram) и фасад CTDA, запускающий шестиэтапный пайплайн одним вызовом. Результаты проверяются сравнением с Ripser до численного совпадения, что позволяет надежно вычислять метрики по диаграммам.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Рыночная микроструктура в MQL5 (Часть 7): Классификация рыночных режимов Рыночная микроструктура в MQL5 (Часть 7): Классификация рыночных режимов
Мы объединяем одиннадцать показателей минутной рыночной микроструктуры из частей 2-6 в итоговую метку режима с оценкой достоверности и направлением. Функция RegimeClassifier(), работающая на основе правил, определяет один из шести режимов — нормальный, стрессовый, шумовой, информированный, трендовый или возврат к среднему. Пороги получены эмпирически по 514 сессиям NQ на таймфрейме M1 (май 2024 г. - май 2026 г.). Результат работы включает MARKET_REGIME, RegimeAnalysis и PopulateRegimeAnalysis(): один вызов позволяет получить данные для расчета размера позиции, размещения стоп-лоссов и фильтрации сигналов.