English 中文 Español Deutsch 日本語 Português
Кроссплатформенный торговый советник: Ордера

Кроссплатформенный торговый советник: Ордера

MetaTrader 5Интеграция | 27 сентября 2016, 12:12
5 088 0
Enrico Lambino
Enrico Lambino

Оглавление


Введение

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

Условия

Есть много различий в том, как MetaTrader 4 и MetaTrader 5 обрабатывают торговые запросы из терминала. Обсуждая детали обработки торговых запросов серверами, мы должны рассмотреть три различных режима в обеих торговых платформах: (1) MetaTrader 4, (2) режим неттинга в MetaTrader 5, и (3) режим хеджирования в MetaTrader 5.

MetaTrader 4

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

Более сложная ситуация возникает, когда ордер закрывается частично. Эта операция осуществляется с помощью функции OrderClose, с указанием меньшего размера лота, чем общий размер лота в запросе. Когда отправляется такой торговый запрос, ордер закрывается на определенную величину размера лота, указанную в вызове функции. Оставшийся объем лота после этого останется на рынке как новый ордер того же типа, что и частично закрытый. Поскольку функция OrderClose только возвращает логическую переменную, не существует другого быстрого способа получить новый тикет, кроме как пересмотреть список ордеров, активных в данный момент на счете. Обратите внимание, что невозможно получить новый тикет, используя только функцию OrderClose. Функция возвращает логическую переменную, в то время как функции, подобные OrderSend, в MQL4 возвращают валидный тикет после успешной транзакции.

MetaTrader 5 (неттинг)

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

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

 Artifact
 MQL5 (Неттинг)
MQL4 (Примерный эквивалент)
ОрдерТорговый запрос (отложенный или рыночный)Торговый запрос (отложенный или рыночный)
 Сделка Сделка (сделки), совершенная на основе одного ордера (рыночного или сработавшего отложенного ордера)  Единственный рыночный ордер, отражаемый в торговом терминале
 Позиция Сделки (объединенные) Сумма всех рыночных ордеров в торговом терминале (применяются типы ордеров)

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

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

В MetaTrader 4 такой опции нет: ордер либо полностью исполняется, или не исполняется вообще (fill or kill).

Один из существенных недостатков этого режима в MetaTrader 5 состоит в том, что он не позволяет хеджировать. Тип позиции на данном инструменте может меняться. Например, если есть длинная позиция объемом 0,1 лота по символу, ввод ордера на продажу объемом в 1 лот преобразует позицию по этому инструменту в короткую, с объемом 0,9 лота.

MetaTrader 5 (Хеджирование)

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

Artifact
MQL5 (Неттинг)
MQL4 (Примерный эквивалент)
ОрдерТорговый запрос (отложенный или рыночный)Торговый запрос (отложенный или рыночный)
СделкиСделка (сделки) совершаются на основании единственного ордераРыночные ордера отражены в торговом терминале
ПозицияТрейды (объединенные) основываются на единственном торговом запросе.Ордер отображается в торговом терминале

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

class COrderBase : public CObject
  {
protected:
   bool              m_closed;
   bool              m_suspend;
   long              m_order_flags;
   int               m_magic;
   double            m_price;
   ulong             m_ticket;
   ENUM_ORDER_TYPE   m_type;
   double            m_volume;
   double            m_volume_initial;
   string            m_symbol;
public:
                     COrderBase(void);
                    ~COrderBase(void);
   //--- getters and setters     
   void              IsClosed(const bool);
   bool              IsClosed(void) const;
   void              IsSuspended(const bool);
   bool              IsSuspended(void) const;
   void              Magic(const int);
   int               Magic(void) const;
   void              Price(const double);
   double            Price(void) const;
   void              OrderType(const ENUM_ORDER_TYPE);
   ENUM_ORDER_TYPE   OrderType(void) const;
   void              Symbol(const string);
   string            Symbol(void) const;
   void              Ticket(const ulong);
   ulong             Ticket(void) const;
   void              Volume(const double);
   double            Volume(void) const;
   void              VolumeInitial(const double);
   double            VolumeInitial(void) const;
   //--- output
   virtual string    OrderTypeToString(void) const;
   //--- static methods
   static bool       IsOrderTypeLong(const ENUM_ORDER_TYPE);
   static bool       IsOrderTypeShort(const ENUM_ORDER_TYPE);
  };

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

Идентификатор сделки (тикет)

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

Операция
 MQL4MQL5 (Неттинг)
 MQL5 (Хеджинг)
Отправка ордераТикет нового ордера
Новый тикет позиции (для вновь открываемой) или Тикет существующей позиции (для позиции, которая уже существует).
Тикет новой позиции
Частичное закрытиеТикет нового ордера
Тот же тикет (если есть остаток), в противном случае - N/A
Тот же тикет

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

Есть еще одна проблема, связанная с частичным закрытием рыночного ордера (в MQL4) или позиции (в MQL5). Как уже говорилось ранее, в MQL4, когда конкретный тикет закрыт не в полном объеме (OrderLots), тикет, представляющий трейд, закрывается, а оставшемуся объему будет назначен новый тикет того же типа, что и частично закрытый. В MQL5 есть небольшое отличие. В режиме неттинга, чтобы закрыть позицию (частично или полностью), происходит трейд в противоположном направлении (buy для sell, или sell для buy). В режиме хеджирования процесс больше похож на MQL4 (OrderClose против PositionClose в CTrade), но, в отличие от MQL4, частичное закрытие позиции не вызовет никаких изменений в идентификаторе, который ее представляет.

Один из способов решения этой проблемы — разбить имплементацию представления идентификаторов определенных сделок по двум платформам. Поскольку тикет ордера не меняется в MetaTrader 5, мы можем просто присвоить ему типичную числовую переменную. Для версии MetaTrader 4 будем использовать экземпляр класса CArrayInt для хранения номеров тикетов. Для COrderBase (и, следовательно, для версии MQL5 COrder), будет использоваться следующий код метода Ticket:

COrderBase::Ticket(const ulong value)
  {
   m_ticket=value;
  }

В версии для MQL4 этот метод будет переопределен следующим кодом:

COrder::Ticket(const ulong ticket)
  {
   m_ticket_current.InsertSort((int)ticket);   
  }

Состояния

Для ордеров в кроссплатформенном советнике возможны, по крайней мере, два состояния:

  • Закрытый

  • Приостановленный

Они очень похожи, но между ними есть фундаментальная разница. Закрытое состояние ордера демонстрирует, что он уже закрыт, и советник должен заархивировать его в своих внутренних данных. Грубый эквивалент этого процесса в MQL4 — отправка ордера в историю. Приостановленное состояние означает только, что советнику не удалось закрыть ордер или один из стоп-ордеров, связанных с ним. В этом случае советник может попытаться закрыть ордер (и его стопы) снова, до тех пор, пока он не будет полностью закрыт.

Объём

В MQL4 расчет объема происходит просто. Всякий раз, когда советник отправляет торговый запрос, в него включен также и объем. Соответственно, этот запрос будет либо отклонен, либо принят. Это — эквивалент политики исполнения Fill or Kill (FOK) в MQL5, которая является настройкой по умолчанию для торговых объектов (CTrade и CExpertTrade). Получение общей функции может дать нам именно эту политику. А чтобы сделать обработку объема совместимой между MQL4 и MQL5, одним из способов будет получение объема экземпляра COrder на основе объема самого торгового запроса. Это будет означать, что в версии MQL5 мы должны будем придерживаться политики FOK. Возможно использовать и другие политики исполнения, но результаты будут немного отличаться (то есть, количество экземпляров COrder в версии MQL5 на данном испытании одного и того же EA может быть больше).

Хранилище ордеров

Если советник обрабатывает более одного экземпляра COrder, может понадобиться какой-либо метод организации. Один из классов, который может помочь в этом, — хранилище ордеров, или COrders. Класс наследуется от CArrayObj и хранит в себе экземпляры COrder. Он делает возможным удобное хранение и поиск трейдов, проведенных советников. Основной шаблон для описанного класса показан ниже:

#include <Arrays\ArrayObj.mqh>
#include "OrderBase.mqh"
class CExpertAdvisor;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class COrdersBase : public CArrayObj
  {
public:
                     COrdersBase(void);
                    ~COrdersBase(void);
   virtual bool      NewOrder(const ulong,const string,const int,const ENUM_ORDER_TYPE,const double,const double);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
COrdersBase::COrdersBase(void)
  {
   if(!IsSorted())
      Sort();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
COrdersBase::~COrdersBase(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
         order.Magic(magic);
   return false;
  }
//+------------------------------------------------------------------+
#ifdef __MQL5__
#include "..\..\MQL5\Order\Orders.mqh"
#else
#include "..\..\MQL4\Order\Orders.mqh"
#endif
//+------------------------------------------------------------------+

Поскольку его основная функция — хранение экземпляров класса COrder, он должен иметь способы добавления этих экземпляров указанного класса. По умолчанию для этого используется метод Add класса CArrayObj. Однако он не всегда идеален, так как экземпляр COrder требуется инстанцировать. Для этого мы могли бы иметь метод NewOrder, который бы уже создал новый экземпляр класса COrder и автоматически добавлял бы его в качестве элемента массива:

bool COrdersBase::NewOrder(const ulong ticket,const string symbol,const int magic,const ENUM_ORDER_TYPE type,const double volume,const double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
         order.Magic(magic);
   return false;
  }

Теперь, когда основной шаблон уже создан, к этому классу могут быть добавлены другие методы. Один из примеров — метод OnTick. В этом методе класс-хранилище (контейнер) будет просто проходиться по элементам, которые в нем хранятся. Другая возможность — сделать так, чтобы класс COrder также содержал метод OnTick. Затем он может быть закодирован таким образом, что этот метод в COrders будет вызываться на каждом тике.

Пример

Наш пример кода будет пробовать открыть длинную позицию. После того, как позиция введена на рынок, информация о сделке будет потом сохраняться в экземляре COrder. Это достигается путем вызова метода NewOrder класса COrders (его задача — создание экземпляра COrder).

Обе версии будут использовать экземпляры торгового объекта (CExpertTradeX), объекта ордера (COrders) и объекта символа. (CSymbolInfo). В обработчике OnTick торгового советника торговый объект попытается войти в длинную позицию, используя метод Buy. Единственное различие между двумя версиями (MQL4 и MQL5) — в том, как будет извлекаться информация по сделкам. В версии MQL5 детали по сделкам извлекаются с использованием HistoryOrderSelect и других родственных функций. Тикет ордера извлекается с помощью метода ResultOrder торгового объекта. Имплементация этой версии показана ниже:

ulong retcode=trade.ResultRetcode();
ulong order = trade.ResultOrder();
 if(retcode==TRADE_RETCODE_DONE)
 {
  if(HistoryOrderSelect(order))
  {
   ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET);
   ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC);
   string symbol = HistoryOrderGetString(order,ORDER_SYMBOL);
   double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL);
   double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN);
   ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)HistoryOrderGetInteger(order,ORDER_TYPE);
   orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price);
  }
 }

Торговый объект в MQL4 имеет меньше функций, чем в MQL5. Можно расширить торговый объект для этой версии, или просто перебрать все активные ордера на счете, чтобы получить только что открытый ордер:

for(int i=0;i<OrdersTotal();i++)
{
 if(!OrderSelect(i,SELECT_BY_POS))
  continue;
 if(OrderMagicNumber()==12345)
  orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice());
}

Полный код основного заголовочного файла показан ниже:

(test_orders.mqh)

#include <MQLx-Orders\Base\Trade\ExpertTradeXBase.mqh>
#include <MQLx-Orders\Base\Order\OrdersBase.mqh>
CExpertTradeX trade;
COrders orders;
CSymbolInfo symbolinfo;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!symbolinfo.Name(Symbol()))
     {
      Print("failed to initialize symbol");
      return INIT_FAILED;
     }
   trade.SetSymbol(GetPointer(symbolinfo));
   trade.SetExpertMagicNumber(12345);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(!symbolinfo.RefreshRates())
   {
      Print("cannot refresh symbol");
      return;
   }   
   if(trade.Buy(1.0,symbolinfo.Ask(),0,0))
     {
#ifdef __MQL5__
      int retcode=trade.ResultRetCode();
      ulong order = trade.ResultOrder();
      if(retcode==TRADE_RETCODE_DONE)
        {
         if(HistoryOrderSelect(order))
           {
            ulong ticket=HistoryOrderGetInteger(order,ORDER_TICKET);;
            ulong magic=HistoryOrderGetInteger(order,ORDER_MAGIC);
            string symbol = HistoryOrderGetString(order,ORDER_SYMBOL);
            double volume = HistoryOrderGetDouble(order,ORDER_VOLUME_INITIAL);
            double price=HistoryOrderGetDouble(order,ORDER_PRICE_OPEN);
            ENUM_ORDER_TYPE order_type=order_type;
            m_orders.NewOrder((int)ticket,symbol,(int)magic,order_type,volume,price);
           }
        }
#else
      for(int i=0;i<OrdersTotal();i++)
        {
         if(!OrderSelect(i,SELECT_BY_POS))
            continue;
         if(OrderMagicNumber()==12345)
            orders.NewOrder(OrderTicket(),OrderSymbol(),OrderMagicNumber(),(ENUM_ORDER_TYPE)OrderType(),OrderLots(),OrderOpenPrice());
        }
#endif
     }
   Sleep(5000);
   ExpertRemove();
  }
//+------------------------------------------------------------------+

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

(test_orders.mq4 и test_orders.mq5)

#include "test_orders.mqh"

Запуск советника в платформах дает следующие записи в логе.

В MetaTrader 4 :

Expert test_orders EURUSD,H1: loaded successfully
test_orders EURUSD,H1: initialized
test_orders EURUSD,H1: open #125001338 buy 1.00 EURUSD at 1.10684 ok
test_orders EURUSD,H1: ExpertRemove function called
test_orders EURUSD,H1: uninit reason 0
Expert test_orders EURUSD,H1: removed

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

В MetaTrader 5 генерируется почти такой же файл логов:

Experts    expert test_orders (EURUSD,M1) loaded successfully
Trades    '3681006': instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades    '3681006': accepted instant buy 1.00 EURUSD at 1.10669 (deviation: 10)
Trades    '3681006': deal #75334196 buy 1.00 EURUSD at 1.10669 done (based on order #90114599)
Trades    '3681006': order #90114599 buy 1.00 / 1.00 EURUSD at 1.10669 done in 275 ms
Experts    expert test_orders (EURUSD,M1) removed

В отличие от MetaTrader 4, сообщения лога находятся во вкладке "Журнал" окна "Терминал" (а не во вкладке "Эксперты").

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

Расширения

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

1. Исходные стоп-лосс и тейк-профит для сделок.

2. Модификации стоп-уровней (к примеру, безубыток, Трейлинг-Стоп или любой пользовательский метод).

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

Заключение

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

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

Прикрепленные файлы |
Orders.zip (208.87 KB)
Основы программирования на MQL5: Файлы Основы программирования на MQL5: Файлы
Статья-практикум по работе с файлами в MQL5. Читайте, выполняйте несложные задания, и к концу статьи вы обретете не только теоретические знания, но и практические навыки по работе с файлами в MQL5.
Быстрая оценка сигнала: торговая активность, графики просадки/загрузки и распределения MFE/MAE Быстрая оценка сигнала: торговая активность, графики просадки/загрузки и распределения MFE/MAE
При поиске Сигнала подписчики в первую очередь ориентируются на общий прирост на торговом счете Поставщика, и это, в общем-то, логично. Но при этом также важно принимать во внимание потенциальные риски, которые несет конкретная торговая стратегия. В этой статье мы покажем, как просто и наглядно можно оценить заинтересовавший Сигнал с помощью нескольких показателей.
Нейросеть: Самооптимизирующийся советник Нейросеть: Самооптимизирующийся советник
Возможно ли создать советник, который согласно командам кода автоматически оптимизировал бы критерии открытия и закрытия позиций с определенной периодичностью? Что произойдет, если реализовать в советнике нейросеть (многослойный персептрон), которая, будучи модулем, анализировала бы историю и оценивала стратегию? Можно дать коду команду на ежемесячную (еженедельную, ежедневную или ежечасную) оптимизацию нейросети с последующим продолжением работы. Таким образом возможно создать самооптимизирующийся советник.
Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 3) Графические интерфейсы X: Обновления для библиотеки Easy And Fast (build 3)
В этой статье представлена следующая версия библиотеки Easy And Fast (версия 3). Исправлены некоторые недоработки и добавлены новые возможности. Подробнее читайте далее в статье.