Русский 中文 Español Deutsch 日本語 Português
Ready-made Expert Advisors from the MQL5 Wizard work in MetaTrader 4

Ready-made Expert Advisors from the MQL5 Wizard work in MetaTrader 4

MetaTrader 5Integration | 16 May 2017, 14:01
13 539 3
Stanislav Korotky
Stanislav Korotky

The MetaTrader 4 and MetaTrader 5 client terminals allow users to create prototypes of MQL programs using the built-in MQL Wizard. The Wizards of the two terminal versions are similar, but still have one important difference. The MetaTrader 5 Wizard can generate ready-to-use Expert Advisor, while this option is not available in the MetaTrader 4 Wizard. The reason for that is that such ready-to-use Expert Advisors are based on the Standard MQL Library classes, i.e. a set of header files available in the terminal. MetaTrader 4 also has a standard library, which however does not include no trade classes from MQL5. In particular, there are no classes for preparing and sending trading orders, calculating signals based on indicator values or price action, using trailing stop, and applying money management. All these functions constitute a necessary basis for building automatically generated Expert Advisors.

This situation can be explained by the gradual development of MQL5. The new language appeared originally in MetaTrader 5, therefore the standard library of classes was developed for this terminal. Later, MQL5 was integrated into MetaTrader 4. But trading functions are very different in the APIs of the two terminal, that is why the standard library was moved to MT4 only partially, while trade classes were skipped. As a result, the MetaTrader 4 Wizard does not provide the possibility to generate ready-made Expert Advisors.

At the same time, MetaTrader 4 is still popular, and the ability to generate ready-made Expert Advisors for this platform would be very useful. Since new functions in MetaTrader 4 are no longer added, while only errors are being corrected in new versions, there is little chance of having further improvements in its Wizard. However, we can use the MetaTrader 5 Wizard, and then transfer the resulting code to MetaTrader 4. What is needed for this code to work is a set of trade classes from the Standard Library adapted to the original MQL API MetaTrader 4. In other words, we need to copy from the MetaTrader 5 Standard Library classes that are not available in MetaTrader 4, and implement emulation of the fifth version trading environment for them.

To understand the current material, you need to know the principles of trading operations in MetaTrader 5, the meaning of orders, deals and positions. If you are not familiar with this version of the terminal, it is highly recommended to read the article "Orders, Positions, and Deals in MetaTrader 5".

Planning

Any work is best done when adhering to a certain preconceived plan. An example of this approach, which is used in development, is the so-called waterfall model. This model is good for our case, when in addition to development, its description is added in the form of this article. However, in practice it is more efficient to port MQL5 code to MQL4 (or vice versa) using one of flexible approaches, such as extreme programming. Its motto is less planning and more action. Literally this means that you can take the source code, try to compile it and then correct occurring errors. The plan that I propose in this article did not appear immediately. It was formed gradually, based on the "hints" generated by the compiler.

When comparing the libraries of two terminals, we can see that version 4 does not have the Trade, Expert and Models folders. So the main task is to port all classes from these folders to the fourth version. In addition to them, we will obviously need to correct something in the Indicators folder. It is available in the version 4 library, but the principles of operations with indicators in these terminals are different. However, in any case, you should adhere to the principle of the least edits made to the library files, because it is updated from time to time, and in this case we will need to customize our edits to the official ones.

All copied files refer to some extent to the trading MQL API of the fifth version. Therefore, we will need to develop a more or less complete set of definitions and functions preserving the same programming interface and converting all calls to the inherited MQL API of the fourth version. Let's consider in detail what should be included into the emulated trading environment. Let's start with the types, since they are the bricks used to construct the building, i.e. the algorithm and the program itself.

The simplest types are enumerations. They are used in most functions directly or indirectly, through structures. Therefore, we will adapt the classes in the following order: enumeration, structures, constants, functions.

Enumerations

Some of the necessary enumerations have already been transferred to MetaTrader 4. For example, order properties: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE, ENUM_ORDER_PROPERTY_STRING. On the one hand, it seems to be convenient, but on the other hand not all of these enumerations are defined exactly as in MetaTrader 5, which creates difficulties.

For example, ENUM_ORDER_TYPE in MetaTrader 5 contains more types of orders than in MetaTrader 4. If we leave ENUM_ORDER_TYPE as is, we will get compilation errors, because the copied code will refer to missing elements. The enumeration cannot be re-defined. Therefore, the easiest solution is the macro definition for the preprocessor, like this:

// ENUM_ORDER_TYPE extension
#define ORDER_TYPE_BUY_STOP_LIMIT ((ENUM_ORDER_TYPE)6)
#define ORDER_TYPE_SELL_STOP_LIMIT ((ENUM_ORDER_TYPE)7)
#define ORDER_TYPE_CLOSE_BY ((ENUM_ORDER_TYPE)8)

Other enumerations which are not available in MetaTrader 4 can be defined by analogy with MT5, for example:

enum ENUM_ORDER_TYPE_FILLING
{
  ORDER_FILLING_FOK,
  ORDER_FILLING_IOC,
  ORDER_FILLING_RETURN
};

Thus, we should define (or add constants to) the following enumerations. At first glance, it seems that there are too many enumerations. But the process is simple, and we only need to copy them from the documentation (please see links to the appropriate sections below; existing enumerations which require some adjustments are marked with an asterisk).

  • Order
    • ENUM_ORDER_TYPE_TIME
    • ENUM_ORDER_STATE
    • ENUM_ORDER_TYPE_FILLING
    • ENUM_ORDER_TYPE (*)
    • ENUM_ORDER_PROPERTY_INTEGER (*)
    • ENUM_ORDER_PROPERTY_STRING (*)
  • Position
    • ENUM_POSITION_TYPE
    • ENUM_POSITION_PROPERTY_INTEGER
    • ENUM_POSITION_PROPERTY_DOUBLE
    • ENUM_POSITION_PROPERTY_STRING
  • Deal
    • ENUM_DEAL_ENTRY
    • ENUM_DEAL_TYPE
    • ENUM_DEAL_PROPERTY_INTEGER
    • ENUM_DEAL_PROPERTY_DOUBLE
    • ENUM_DEAL_PROPERTY_STRING
  • Trade operation types
    • ENUM_TRADE_REQUEST_ACTIONS

MetaTrader 4 already contains definitions of enumerations describing symbols, such as ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING. Some elements in them are only reserved, but do not operate (see the Documentation). These are the limitations of the MetaTrader 4 platform, and we must accept this. For us, it is only important that there is no need to define these enumerations in the project.

Structures

In addition to enumerations, structures are used in MetaTrader 5 functions. Their definitions can also be taken from the documentation (links to the appropriate sections are given below).

Macro definitions

In addition to the above types, a lot of constants are used in MT5. The easiest way to define these constants in this project, is to use the #define directive of the preprocessor.

Trade Functions

The last and the most important stage of our plan includes trade functions. We will only be able to start implementing them after defining all the above types and constants.

The list of trade functions is impressive. They can be divided into 4 groups:

  • Orders
  • Positions
  • History of Orders
  • History of Deals

Finally, we will use the following simple substitutions:

#define MQL5InfoInteger MQLInfoInteger
#define MQL5InfoString  MQLInfoString

In fact, above are the same functions of the terminal kernel, but their names slightly differ in MQL5 and MQL4.

Before proceeding directly to the implementation, we need to define how to reflect the MetaTrader 5 trading model over the MetaTrader 4 trading model.

Reflection

Let's try to draw a parallel between MetaTrader 5 and MetaTrader 4 entities. It's easier to start with MT4, in which a universal concept of "order" is used. This concept refers practically to everything, including market orders, pending orders, and history of trading operations. In all these cases, the order is in different states. In MT5 market orders are positions, pending orders are orders, and the history is written as deals.‌

In the simplest case, MetaTrader 5 operation is the following. In order to form a position, an order to enter the market is sent to a trade server. To close the position, another order is sent, which is an order to exit the market. Each order is executed within the framework of the corresponding deal, which is added to the trading history. Thus, one MT4 market order should be displayed in an emulated MT5 trading environment as follows:

  • Entry order
  • Entry deal
  • Position
  • Exit order
  • Exit deal

MetaTrader 5 originally was a purely netting platform, in which only one position could exist for one symbol at a moment of time. All orders for the same symbol increased, reduced or completely removed the total traded volume of the symbol, and also changed the total stop loss and take profit levels for that symbol. This mode is not available in MetaTrader 4, and it would have been very difficult to implement this project, if support for hedging hadn't been added to MetaTrader 5. This mode is used in MetaTrader 4: execution of each order leads to the generation of a separate "position" (in terms of MetaTrader 5); several open orders can exist on the same symbol, including opposite orders.

Attention! If you want to compare the operation of generated Expert Advisors in MetaTrader 5 and MetaTrader 4, keep in mind that the hedging mode must be enabled in your MetaTrader 5 terminal. For comparison, it is recommended to use the servers of the same broker.

Implementation

Emulation of the MetaTrader 5 environment

For simplicity, we will place the entire emulated environment, including types, constants and functions, in a single header file MT5Bridge.mqh. According to a good programming style, we would need to arrange them into separate files. This structuring is especially important for large projects, as well as team projects. However, one file is more convenient in terms of distribution and installation.

According to our plan, we need to define all enumerations, constants and structures. This is a routine copying job. There is no need to explain it in more detail, while comments provided at the planning stage are enough. Let us once again check information about Trade functions in the documentation and proceed further to a more complicated part, which includes writing the code for all these functions.‌

Let's start with current operations, which include processing of market and pending orders, as well as positions.

For this purpose, we need the super-universal MT5 function OrderSend.

bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
{

Depending on the request type, we need to use one of MT4 order types in this function.

  int cmd;   
  result.retcode = 0;
  switch(request.type)
  {
    case ORDER_TYPE_BUY:
      cmd = OP_BUY;
      break;
    case ORDER_TYPE_SELL:
      cmd = OP_SELL;
      break;
    case ORDER_TYPE_BUY_LIMIT:
      cmd = OP_BUYLIMIT;
      break;
    case ORDER_TYPE_SELL_LIMIT:
      cmd = OP_SELLLIMIT;
      break;
    case ORDER_TYPE_BUY_STOP:
      cmd = OP_BUYSTOP;
      break;
    case ORDER_TYPE_SELL_STOP:
      cmd = OP_SELLSTOP;
      break;
    default:
      Print("Unsupported request type:", request.type);
      return false;
  }

The passed operation code from the action field allows to process placing, deletion and modification of orders in different ways. For example, opening of a market order or creation of a pending order can be implemented as follows.

  ResetLastError();
  if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)
  {
    if(request.price == 0)
    {
      if(cmd == OP_BUY)
      {
        request.price = MarketInfo(request.symbol, MODE_ASK);
      }
      else
      if(cmd == OP_SELL)
      {
        request.price = MarketInfo(request.symbol, MODE_BID);
      }
    }
    if(request.position > 0)
    {
      if(!OrderClose((int)request.position, request.volume, request.price, (int)request.deviation))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = request.position | 0x8000000000000000;
        result.order = request.position | 0x8000000000000000;
        result.volume = request.volume;
        result.price = request.price;
      }
    }
    else
    {
      int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (int)request.deviation, request.sl, request.tp, request.comment, (int)request.magic, request.expiration);
      if(ticket == -1)
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = ticket;
        result.order = ticket;
        result.request_id = ticket;
        if(OrderSelect(ticket, SELECT_BY_TICKET))
        {
          result.volume = OrderLots();
          result.price = OrderOpenPrice() > 0 ? OrderOpenPrice() : request.price;
          result.comment = OrderComment();
          result.ask = MarketInfo(OrderSymbol(), MODE_ASK);
          result.bid = MarketInfo(OrderSymbol(), MODE_BID);
        }
        else
        {
          result.volume = request.volume;
          result.price = request.price;
          result.comment = "";
        }
      }
    }
  }

The basic work is performed by the usual MT4 function OrderSend with many parameters. After its call, operation results are appropriately written to the output structure.

Note that in MetaTrader 5, an existing market order is closed by opening another order in the opposite direction, and the identifier of the closed position is passed in the position field. In this case, i.e. when the position field is not empty, the above code attempts to close the order using the OrderClose function. Here, the ticket of the order is used as the position identifier. This is logically correct, since in MT4 each order creates its own position. The same ticket is used for the deal.

As for the virtual order that closes the position (which actually does not exist here), the original number supplemented with a high-order bit set to 1 is used artificially as its ticket number. This will be used later when enumerating orders and deals.

Now let's see how we can implement the modification of levels of an open position.

  else if(request.action == TRADE_ACTION_SLTP) // change opened position
  {
    if(OrderSelect((int)request.position, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.position, OrderOpenPrice(), request.sl, request.tp, 0))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_POSITION_CLOSED;
    }
  }

It is quite obvious that OrderModify is used for that purpose.

The same function is also used to modify a pending order.

  else if(request.action == TRADE_ACTION_MODIFY) // change pending order
  {
    if(OrderSelect((int)request.order, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.order, request.price, request.sl, request.tp, request.expiration))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.price = request.price;
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_INVALID_ORDER;
    }
  }

Deletion of a pending order is performed by the standard OrderDelete function.

  else if(request.action == TRADE_ACTION_REMOVE)
  {
    if(!OrderDelete((int)request.order))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }

Finally, a Close By operation (closing one position using an opposite one) is equivalent to closure of opposite orders in the context of MetaTrader 4.

  else if(request.action == TRADE_ACTION_CLOSE_BY)
  {
    if(!OrderCloseBy((int)request.position, (int)request.position_by))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }
  return true;
}

In addition to OrderSend, MetaTrader 5 provides an asynchronous OrderSendAsync function. We will not implement it, and will disable all cases of using asynchronous mode in the library, i.e. we actually replace it with the synchronous version.

Sending of orders is often accompanied by calls of three other functions: OrderCalcMargin, OrderCalcProfit, OrderCheck.‌

Here is one of the versions of implementing them using tools available in MetaTrader 4.

int EnumOrderType2Code(int action)

{   // ORDER_TYPE_BUY/ORDER_TYPE_SELL and derivatives   return (action % 2 == 0) ? OP_BUY : OP_SELL; }

bool OrderCalcMargin(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price,   double         &margin   ) {   int cmd = EnumOrderType2Code(action);   double m = AccountFreeMarginCheck(symbol, cmd, volume);   if(m <= 0 || GetLastError() == ERR_NOT_ENOUGH_MONEY)   {     return false;   }   margin = AccountFreeMargin() - m;   return true; }

bool OrderCalcProfit(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price_open,   double          price_close,   double         &profit   ) {   int cmd = EnumOrderType2Code(action);   if(cmd > -1)   {     int points = (int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT));     if(cmd == OP_SELL) points = -points;     profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT));     return true;   }   return false; } bool OrderCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result) {   if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT)   || request.volume < MarketInfo(request.symbol, MODE_MINLOT)   || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP))   {     result.retcode = TRADE_RETCODE_INVALID_VOLUME;     return false;   }   double margin;   if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin))   {     result.retcode = TRADE_RETCODE_NO_MONEY;     return false;   }   if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)   && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET   && (request.sl != 0 || request.tp != 0))   {     result.retcode = TRADE_RETCODE_INVALID_STOPS;     return false;   }   result.balance = AccountBalance();   result.equity = AccountEquity();   result.profit = AccountEquity() - AccountBalance();   result.margin = margin;   result.margin_free = AccountFreeMargin();   result.margin_level = 0;   result.comment = "";   return true; }

The following built-in functions are actively used here: AccountEquity, AccountFreeMargin, AccountFreeMarginCheck, as well as the symbol point value and its other settings obtained by calling MarketInfo.

To obtain the total number of positions, it is enough to return the number of open market orders.

int PositionsTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

To obtain the symbol of a position by its number, it is necessary to cycle through all orders, counting only market orders.

string PositionGetSymbol(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderSymbol();
        }
        count++;
      }
    }
  }
  return "";
}

Similarly, we form a  function for obtaining the ticket of a position by its number.

ulong PositionGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

To select a position by the symbol name, we will also cycle through market orders and will stop at the first one with the matching symbol.

bool PositionSelect(string symbol)
{
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderSymbol() == symbol && (OrderType() <= OP_SELL))
      {
        return true;
      }
    }
  }
  return false;
}

Position selection by ticket can be implemented without a cycle.

bool PositionSelectByTicket(ulong ticket)
{
  if(OrderSelect((int)ticket, SELECT_BY_TICKET))
  {
    if(OrderType() <= OP_SELL)
    {
      return true;
    }
  }
  return false;
}

The properties of a selected position should be returned by three functions used in MetaTrader 5: _GetDouble, _GetInteger, _GetString. Here we present their implementation for positions. These functions will look very similar for orders and deals, so we will not analyze them here. However, their code is available in the attached file.

// Position = order, only OP_BUY or OP_SELL
ENUM_POSITION_TYPE Order2Position(int type)
{
  return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
}

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, long &long_var)
{
  switch(property_id)
  {
    case POSITION_TICKET:
    case POSITION_IDENTIFIER:
      long_var = OrderTicket();
      return true;
    case POSITION_TIME:
    case POSITION_TIME_UPDATE:
      long_var = OrderOpenTime();
      return true;
    case POSITION_TIME_MSC:
    case POSITION_TIME_UPDATE_MSC:
      long_var = OrderOpenTime() * 1000;
      return true;
    case POSITION_TYPE:
      long_var = Order2Position(OrderType());
      return true;
    case POSITION_MAGIC:
      long_var = OrderMagicNumber();
      return true;
  }
  return false;
}

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, double &double_var)
{
  switch(property_id)
  {
    case POSITION_VOLUME:
      double_var = OrderLots();
      return true;
    case POSITION_PRICE_OPEN:
      double_var = OrderOpenPrice();
      return true;
    case POSITION_SL:
      double_var = OrderStopLoss();
      return true;
    case POSITION_TP:
      double_var = OrderTakeProfit();
      return true;
    case POSITION_PRICE_CURRENT:
      double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK);
      return true;
    case POSITION_COMMISSION:
      double_var = OrderCommission();
      return true;
    case POSITION_SWAP:
      double_var = OrderSwap();
      return true;
    case POSITION_PROFIT:
      double_var = OrderProfit();
      return true;
  }
  return false;
}

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case POSITION_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case POSITION_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

Similar to positions that are actually market orders, we need to implement a set of functions for processing pending orders. However, there is one difficulty: We cannot implement the OrdersTotal function and other OrderGet_ functions, because they are already defined in the kernel, and built-in functions cannot be overridden. The compiler will return the following error:

'OrderGetString' - override system function MT5Bridge.mqh

Therefore, we have to use other names for all functions, whose names start with Order_. A logical solution will be to start their names with PendingOrder_, because they only process pending orders. For example:

int PendingOrdersTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

Then, in the standard library code, we will need to replace all calls to these new functions from MT5Bridge.mqh.

The OrderGetTicket function returning the order ticket by the number does not exist in MetaTrader 4, so we don't need to change its name and will use it in accordance with the MetaTrader 5 API.‌

ulong OrderGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

The OrderSelect function exists in MetaTrader 4 with a larger list of parameters as compared to MetaTrader 5, so we leave its calls and add the required SELECT_BY_TICKET parameter.

The full implementation of the function that reads the properties of pending orders is available in the attached header file.‌

Now let's look at functions for working with the history of orders and deals. Their implementation will be somewhat more complicated. The following variant (which is one of many possible variants) was chosen because of its simplicity.

Each market order in MetaTrader 4 is displayed in history as two MT5-style orders: entry and exit. In addition, the history must contain a corresponding pair of deals. Pending orders are displayed as is. The history will be stored in two arrays with tickets.

int historyDeals[], historyOrders[];

They will be filled by the HistorySelect function from MQL5 API.

bool HistorySelect(datetime from_date, datetime to_date)
{
  int deals = 0, orders = 0;
  ArrayResize(historyDeals, 0);
  ArrayResize(historyOrders, 0);
  for(int i = 0; i < OrdersHistoryTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
    {
      if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date)
      {
        if(OrderType() <= OP_SELL) // deal
        {
          ArrayResize(historyDeals, deals + 1);
          historyDeals[deals] = OrderTicket();
          deals++;
        }
        ArrayResize(historyOrders, orders + 1);
        historyOrders[orders] = OrderTicket();
        orders++;
      }
    }
  }
  return true;
}

Once the arrays are filled, we can obtain the size of the history.

int HistoryDealsTotal()
{
  return ArraySize(historyDeals) * 2;
}

int HistoryOrdersTotal()
{
  return ArraySize(historyOrders) * 2;
}

The sizes of arrays are multiplied by 2, since each order of MetaTrader 4 is represented as two orders or two deals in MetaTrader 5. It is not necessary for pending orders, but in order to preserve the commonality of the approach, we still reserve 2 tickets, one of which will not be used (see. the HistoryOrderGetTicket function below). The market entry deal will have the same ticket used in MetaTrader 4 order that generated this deal. For an exit deal, this ticket will be supplemented with a single high-order bit.

ulong HistoryDealGetTicket(int index)
{
  if(OrderSelect(historyDeals[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    // odd - enter - positive, even - exit - negative
    return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
  }
  return 0;
}

Even numbers in the history always contain entry tickets (real), odd ones are exit tickets (virtual).

The case is a little more complicated for orders, since there may be pending ones among them, which are displayed as is. In this case, the even number will return the correct ticket of the pending order, and the next odd number will return 0.

ulong HistoryOrderGetTicket(int index)
{
  if(OrderSelect(historyOrders[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    if(OrderType() <= OP_SELL)
    {
      return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
    }
    else if(index % 2 == 0) // pending order is returned once
    {
      return OrderTicket();
    }
    else
    {
      Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> 0");
    }
  }
  return 0;
}

Selection of a deal by ticket is implemented taking into account this feature with the addition of the high-order bit (here it needs to be skipped).

bool HistoryDealSelect(ulong ticket)
{
  ticket &= ~0x8000000000000000;
  return OrderSelect((int)ticket, SELECT_BY_TICKET, MODE_HISTORY);
}

For the order, everything is completely analogous.

#define HistoryOrderSelect HistoryDealSelect

Having a deal selected using HistoryDealSelect or HistoryDealGetTicket, we can write an implementation of functions for accessing the deal properties.

#define REVERSE(type) ((type + 1) % 2)

ENUM_DEAL_TYPE OrderType2DealType(const int type)
{
  static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -1, -1, -1, -1, DEAL_TYPE_BALANCE};
  return types[type];
}

bool HistoryDealGetInteger(ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, long &long_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  if(OrderSelect((int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY))
  {
    switch(property_id)
    {
      case DEAL_TICKET:
      case DEAL_ORDER:
      case DEAL_POSITION_ID:
        long_var = OrderTicket();
        return true;
      case DEAL_TIME:
        long_var = exit ? OrderCloseTime() : OrderOpenTime();
        return true;
      case DEAL_TIME_MSC:
        long_var = (exit ? OrderCloseTime() : OrderOpenTime()) * 1000;
        return true;
      case DEAL_TYPE:
        long_var = OrderType2DealType(exit ? REVERSE(OrderType()) : OrderType());
        return true;
      case DEAL_ENTRY:
        long_var = exit ? DEAL_ENTRY_OUT : DEAL_ENTRY_IN;
        return true;
      case DEAL_MAGIC:
        long_var = OrderMagicNumber();
        return true;
    }
  }
  return false;
}
  
bool HistoryDealGetDouble(ulong ticket_number, ENUM_DEAL_PROPERTY_DOUBLE property_id, double &double_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  switch(property_id)
  {
    case DEAL_VOLUME:
      double_var = OrderLots();
      return true;
    case DEAL_PRICE:
      double_var = exit ? OrderClosePrice() : OrderOpenPrice();
      return true;
    case DEAL_COMMISSION:
      double_var = exit? 0 : OrderCommission();
      return true;
    case DEAL_SWAP:
      double_var = exit ? OrderSwap() : 0;
      return true;
    case DEAL_PROFIT:
      double_var = exit ? OrderProfit() : 0;
      return true;
  }
  return false;
}

bool HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case DEAL_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case DEAL_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

I hope the idea is clear. The group of functions for working with orders in the history.

Changes in the Standard Library files

Some necessary edits of the library have already been discussed during the implementation of functions. You can compare files from the MetaTrader 5 delivery and files created within this project, to get a complete list of changes. Further, only the most important points are considered, while comments on minor corrections are omitted. Many files have a new #include directive to connect MT5Bridge.mqh.

Table of main changes in the Standard Library files


File/Method Changes
Trade.mqh SetAsyncMode A line with the asynchronous mode has been removed, because this mode is not supported
SetMarginMode Mode ACCOUNT_MARGIN_MODE_RETAIL_HEDGING is explicitly specified
OrderOpen A combination of flags setting expiration mode is explicitly specified as SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED
OrderTypeCheck Cases of processing nonexistent types ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT are excluded
OrderSend A call of the nonexistent OrderSendAsync function is removed
 
OrderInfo.mqh All calls of OrderGetInteger, OrderGetDouble, OrderGetString are replaced with function with the same names and the PendingOrder prefix
All calls of OrderSelect(m_ticket) are replaced with OrderSelect((int)m_ticket, SELECT_BY_TICKET)
 
PositionInfo.mqh FormatPosition
SelectByIndex
The margin mode ACCOUNT_MARGIN_MODE_RETAIL_HEDGING is set
 
SymbolInfo.mqh Refresh Many checks that are not supported in MetaTrader 4 are removed
 
AccountInfo.mqh MarginMode Returns the ACCOUNT_MARGIN_MODE_RETAIL_HEDGING constant
 
Expert.mqh TimeframeAdd
TimeframesFlags
Removed unsupported timeframes
 
ExpertBase.mqh Added #include <Indicators\IndicatorsExt.mqh>
SetMarginMode Unconditionally set to ACCOUNT_MARGIN_MODE_RETAIL_HEDGING


The IndicatorsExt.mqh file is required for correcting minor errors in the standard Indicators.mqh file. In addition, it includes another header file TimeSeriesExt.mqh necessary for indicators.

The TimeSeriesExt.mqh file contains descriptions of classes that are needed to trade in the MetaTrader 5 style, and which are not available in the standard TimeSeries.mqh file in MetaTrader 4.

In particular, these are the following classes: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume. Many of them are just stubs that do nothing (and cannot do anything, because the corresponding functionality is not available in MetaTrader 4).

Testing

By saving the adapted trade classes of the standard library to the Include directory of MetaTrader 4 (preserving the hierarchy of sub-directories), and copying MT5Bridge.mqh to the Include/Trade folder, we can compile and run Expert Advisors generated by the MetaTrader 5 Wizard directly in MetaTrader 4.

The MetaTrader 5 package contains examples of generated Expert Advisors (in the Experts/Advisors folder). We will work with one of these EAs, ExpertMACD.mq5. Let us copy it to the MQL4/Experts folder and re-name to ExpertMACD.mq4. Compilation in the editor should have approximately the following result:

Compilation of an Expert Advisor from the MetaTrader 5 in MetaTrader 4

Compilation of an Expert Advisor from the MetaTrader 5 in MetaTrader 4

As you can see, the library files are connected and are processed without errors and warnings. Of course, the absence of compilation errors does not guarantee the absence of problems in the program logic, but this is a task for further practical testing.

Let us run the compiled Expert Advisor with default settings in the MetaTrader 4 tester.

MetaTrader 4 testing report for the Expert Advisor generated in MetaTrader 5

MetaTrader 4 testing report for the Expert Advisor generated in MetaTrader 5

You can check the log and make sure it does not contain obvious order processing errors.

On the EURUSD M15 chart, the Expert Advisor trading looks good, including the way it sets the stop loss and take profit levels.

A chart window, demonstrating the work of an EA from the MetaTrader 5 Wizard in MetaTrader 4

A chart window, demonstrating the work of an EA from the MetaTrader 5 Wizard in MetaTrader 4

Let us compare it with the results from the MetaTrader 5 Strategy Tester.

MetaTrader 5 testing report for the generated Expert Advisor

MetaTrader 5 testing report for the generated Expert Advisor

Obviously, there are differences. They can be explained by different quotes (for example, MetaTrader 5 uses a floating spread) as well as by different algorithms used in the testers. However, the tests look generally similar: the number of trades and the character of the balance curve are nearly the same.

Of course, the user can generate his own Expert Advisor with a completely arbitrary set of modules in the MetaTrader 5 Wizard, which can then be easily transferred to MetaTrader 4. In particular, during the project testing, we tested Expert Advisors with a trailing stop function and a variable lot size.

Conclusion

We have considered one of the possible ways to migrate Expert Advisors generated using the MetaTrader 5 Wizard to the MetaTrader 4 platform. Its main advantage is the relative ease of implementation, which is based on the fullest possible use of the existing code of trade classes of the MetaTrader 5 standard library. The main drawback is the need to have both terminals on the computer: one of them is used for generating Expert Advisors, and in the second terminal these EAs are executed.

Below are 2 files:

  • An archive with the modified files of the standard library, which should be unpacked preserving the hierarchy of subdirectories in the Include directory of MetaTrader 4. The archive only contains the files that are not available in the terminal package, so there is no possibility of overwriting existing files;
  • The MT5Bridge.mqh file should be copied to Include/Trade.

The library version from MetaTrader 5 build 1545 was used in this article. The standard library can be changed in future builds, such changes can be useful (and you may need to re-merge files with the emulator edits). A perfect solution would be to have the standard library version developed by MetaQuotes, in which the two versions of trade classes implementation for MetaTrader 5 and MetaTrader 4 would be originally combined using conditional compilation directives.

It should be noted that it will not be possible to implement a full emulation of the MetaTrader 5 trading environment in MetaTrader 4. The new terminal provides new features that are not available in the old terminal at the kernel level. Therefore, potentially there can be situations when some of the modules used in the generated Expert Advisors would fail to work.

Also, do not forget that the provided implementation of the emulator is distributed in the beta status and may contain hidden errors. Only a long and versatile testing will help us get the final software product that would be suitable for real trading. The availability of the source code allows us to do this in co-operation.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/3068

Attached files |
MT4-1.1-SL5-b1545.zip (145.99 KB)
MT5Bridge.mqh (32.35 KB)
Last comments | Go to discussion (3)
Chris Mukengeshayi
Chris Mukengeshayi | 26 Oct 2017 at 12:29

Good day Mr Stanislav Korotky, I hope you are doing good.

I have find you article very interesting and I have try to follow your instruction on how make a ready-made MT5 EA be compatible to run in MT4.

But I getting the following error, could you please guide me on how to resolve the error?

what I did after following your instruction was: I just copy the MQL5 source file and the MQL5 Program then follow by pasting it into the main directory of Expert folder

and I got so many error.

Could you tell me how can I proceed on making the generated EA of MQL5 work in MT4?

Bellow are the attached error images.

Stanislav Korotky
Stanislav Korotky | 26 Oct 2017 at 13:20
Chris Lazarius:

what I did after following your instruction was: I just copy the MQL5 source file and the MQL5 Program then follow by pasting it into the main directory of Expert folder

and I got so many error.

I don't understand what you mean. You need to unzip the provided patch for MT4 library into MQL4/Include folder. MT5Bridge.mqh header is also required.

How did you get the source code of your expert adviser in MQL5? Only experts generated by MQL Wizard are supported.

Stanislav Korotky
Stanislav Korotky | 27 Oct 2017 at 00:38

After some investigation of the use-case above I can now make a note:

Don't forget to change your expert file extension from mq5 to mq4 after you copy it to MQL4/Experts folder and before compilation.

MetaEditor can compile both MQL4 and MQL5, but MQL5 internally provides many built-in types which will interfere with emulated types introduced in the MT5Bridge.mqh. This will not and is not intended to compile.

MQL5 Cookbook - Pivot trading signals MQL5 Cookbook - Pivot trading signals
The article describes the development and implementation of a class for sending signals based on pivots — reversal levels. This class is used to form a strategy applying the Standard Library. Improving the pivot strategy by adding filters is considered.
Graphical Interfaces X: Word wrapping algorithm in the Multiline Text box (build 12) Graphical Interfaces X: Word wrapping algorithm in the Multiline Text box (build 12)
We continue to develop the Multiline Text box control. This time our task is to implement an automatic word wrapping in case a text box width overflow occurs, or a reverse word wrapping of the text to the previous line if the opportunity arises.
Comparative Analysis of 10 Trend Strategies Comparative Analysis of 10 Trend Strategies
The article provides a brief overview of ten trend following strategies, as well as their testing results and comparative analysis. Based on the obtained results, we draw a general conclusion about the appropriateness, advantages and disadvantages of trend following trading.
Universal Trend with the Graphical Interface Universal Trend with the Graphical Interface
In this article a universal trend indicator is created based on a number of standard indicators. An additionally created graphical interface allows selecting the type of indicator and adjusting its parameter. The indicator is displayed in a separate window with rows of colored icons.