Los Asesores Expertos desde el Asistente MQL5 funcionan en MetaTrader 4

Stanislav Korotky | 2 mayo, 2017


Los terminales de cliente MetaTrader 4 y MetaTrader 5 ofrecen a sus usuarios la posibilidad de crear fácilmente los prototipos de programas en el lenguaje MQL a través del Asistente incorporado (MQL Wizard). Los Asistentes de ambas versiones de los terminales son muy parecidos, aunque tienen una diferencia importante. En el Asistente para MetaTrader 5 hay un punto de generación de los Asesores Expertos (EA) hechos, mientras que MetaTrader 4 no lo tiene. El hecho es que estos EAs trabajan a base de las clases de la librería estándar MQL, o sea el conjunto de archivos de cabecera que forman parte de la entrega del terminal. En MetaTrader 4, esta librería también existe, pero ella no incluye las clases comerciales desde MQL5. En particular, ahí no existen las clases que se encargan de la preparación y el envío de órdenes comerciales, el cálculo de las señales a base de las indicaciones de los indicadores o la estructura de precios, trailing, gestión de dinero— siendo todo eso la base necesaria para construir los EAs generados automáticamente.

Esta situación se formó históricamente debido al desarrollo gradual de MQL5. El nuevo lenguaje apareció originalmente en MetaTrader 5, y precisamente para este terminal fue diseñada la librería estándar de las clases. Sólo pasado un tiempo, MQL5 fue integrado también en MetaTrader 4, pero debido a que las funciones de trading se diferenciaban considerablemente en API de dos versiones del terminal, la librería estándar fue transferida en el producto anterior no completamente, es decir sin las clases comerciales. Como resultado, en el Asistente para MetaTrader 4 no hay opción para generar los EAs hechos.

Al mismo tiempo, MetaTrader 4 sigue siendo popular, y la posibilidad de generar los EAs hechos le vendría muy bien. Puesto que las funciones nuevas ya no se añaden a MetaTrader 4, simplemente se corrigen los errores, es poco probable que veamos las mejoras en su Asistente. No obstante, nada nos impide usar el Asistente para MetaTrader 5, y luego traspasar el código obtenido en MetaTrader 4. Para que este código empiece a funcionar también ahí, necesitamos un poquitito— el conjunto de las clases comerciales de la librería estándar adoptadas para el MQL anterior de API MetaTrader 4. En otras palabras, desde la librería estándar de MetaTrader 5 hay que copiar las clases que faltan en MetaTrader 4, e implementar la emulación del entorno comercial de quinta versión para ellas.

Para entender el siguiente material, es necesario conocer los principios de las operaciones comerciales en MetaTrader 5, el trabajo de las órdenes, transacciones y posiciones. Si no está familiarizado con esta versión del terminal, se recomienda insistentemente que lea el artículo "Órdenes, posiciones y transacciones en MetaTrader 5".

Planeamiento

Es mejor realizar cualquier trabajo manteniéndose a un plan elaborado de antemano. El ejemplo de este enfoque, que se aplica durante el desarrollo de software, es así llamado el modelo en cascada. Este modelo conviene muy bien para nuestro caso, cuando el desarrollo no sólo se realiza como tal, pero también se describe en un artículo como éste. Sin embargo, en la práctica, resulta más eficaz migrar el código MQL5 a MQL4 (o al revés) usando una de las metodologías ágiles como la programación extrema. Su lema es «menos planes, más hechos». Literalmente, eso significa que se puede coger el código fuente, intentar compilarlo y luego ir corrigiendo consecutivamente todos los errores que surjan. El plan que propongo en este artículo no ha nacido de repente, sino paso a paso, precisamente a partir de los «soplos» del compilador disgustado.

Al comparar las librerías de dos terminales, es fácil de notar que en la versión 4 faltan carpetas Trade, Expert, y Models. Entonces, el trabajo principal será migrar todas las clases ubicadas en estas carpetas a la versión 4. Además de eso, por lo visto tendremos que corregir algo en la carpeta Indicators. Ella existe en la librería de la versión 4, pero los principios de trabajo con los indicadores en ambos terminales son diferentes. No obstante, en cualquier caso, se debe atenerse al principio del menor número posible de correcciones de los archivos de la librería, pues ella se actualiza con regularidad, y entonces habrá que sincronizar, ajustar nuestras correcciones a las oficiales.

Todos los archivos copiados, en mayor o menor medida, hacen referencia a MQL comerciales de la API de la quinta versión. Por eso tendremos que desarrollar un conjunto más o menos completo de las definiciones y funciones, que van a convertir todos los accesos a las MQL heredadas de la API de la cuarta versión, guardando la misma interfaz de programación. Veamos en detalle qué es lo que debe ser incluido en el entorno comercial emulado. Comenzamos con los tipos, ya que son los bloques sobre los cuales va a construirse el edificio, es decir, el algoritmo y el programa.

Los tipos más simples son las enumeraciones. Se utilizan en la mayoría de las funciones directa o indirectamente a través de las estructuras. Por eso el orden de la adopción será el siguiente:enumeraciones, estructuras, constantes, funciones.

Enumeraciones

Una parte de las enumeraciones necesarias ya ha sido traspasada a MetaTrader 4. Por ejemplo, las propiedades de las órdenes: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE, ENUM_ORDER_PROPERTY_STRING. Por un lado, eso parece conveniente, pero por otro, no todas de estas enumeraciones están definidas de la misma manera que en MetaTrader 5, y eso crea dificultades.

Por ejemplo, ENUM_ORDER_TYPE en MetaTrader 5 contiene más tipos de órdenes que en MetaTrader 4. Si dejamos ENUM_ORDER_TYPE tal como es, obtendremos el error de compilación, puesto que el código compilado hace referencia a los aumentos ausentes. Es posible redifinir la enumeración, o completar su definición. Por eso, la opción más simple será la definición de las macros para el preprocesador como éste:

// 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)

Podemos determinar sin vacilar (por analogía con la quinta versión) otras enumeraciones que no existen en MetaTrader 4, por ejemplo:

enum ENUM_ORDER_TYPE_FILLING
{
  ORDER_FILLING_FOK,
  ORDER_FILLING_IOC,
  ORDER_FILLING_RETURN
};

Así, debemos definir (o completar con constantes) las enumeraciones listadas abajo. A primera vista, son muchas, pero el trabajo es trivial, es que basta con copiarlas desde la documentación (los enlaces a los apartados correspondientes se muestran a continuación; el asterisco indica en las enumeraciones existentes que necesitan un «retoque»).

  • Orden (Order)
    • ENUM_ORDER_TYPE_TIME
    • ENUM_ORDER_STATE
    • ENUM_ORDER_TYPE_FILLING
    • ENUM_ORDER_TYPE (*)
    • ENUM_ORDER_PROPERTY_INTEGER (*)
    • ENUM_ORDER_PROPERTY_STRING (*)
  • Posición (Position)
    • ENUM_POSITION_TYPE
    • ENUM_POSITION_PROPERTY_INTEGER
    • ENUM_POSITION_PROPERTY_DOUBLE
    • ENUM_POSITION_PROPERTY_STRING
  • Transacción (Deal)
    • ENUM_DEAL_ENTRY
    • ENUM_DEAL_TYPE
    • ENUM_DEAL_PROPERTY_INTEGER
    • ENUM_DEAL_PROPERTY_DOUBLE
    • ENUM_DEAL_PROPERTY_STRING
  • Tipos de operaciones comerciales
    • li>ENUM_TRADE_REQUEST_ACTIONS

MetaTrader 4 ya contiene las definiciones de las enumeraciones que describen los símbolos, tales como ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING. Algunos elementos sólo están reservados en ellas, pero no funcionan (lo que se refleja en ladocumentación). Es la limitación de la plataforma MetaTrader 4 en comparación con MetaTrader 5, y tenemos que aceptar eso como tal. Lo único que nos importa es que no tenemos que definir estas enumeraciones en el mismo proyecto.

Estructuras

Además de las enumeraciones, en las funciones comerciales de MetaTrader 5 se utilizan las estructuras. Sus definiciones también se puede coger de la documentación (los enlaces a los apartados correspondientes se muestran a continuación).

Macrodefiniciones

Además de los tipos arriba mencionados, los códigos fuente de la versión 5 utilizan muchas constantes que en este proyecto se definen fácilmente mediante la directiva #define del preprocesador.

Funciones comerciales

El último y el más importante punto de nuestro plan representa directamente las funciones comerciales. Podremos comenzar su implementación sólo después de que hayan sido definidos los tipos y constantes mencionados antes.

La lista de funciones comerciales es bastante considerable. Se puede dividir en 4 grupos:

  • Órdenes
  • Posiciones
  • Historial de órdenes
  • Historial de transacciones

Finalmente, necesitaremos las siguientes sustituciones simples:

#define MQL5InfoInteger MQLInfoInteger
#define MQL5InfoString  MQLInfoString

La verdad es que se trata de las mismas funciones del núcleo del terminal, pero sus nombres se diferencian un poco en MQL5 y MQL4.

Antes de empezar directamente con la implementación, tenemos que pensar en cómo mostrar el modelo comercial de MetaTrader 5 para el modelo comercial de MetaTrader 4.

Visualización

Intentaremos trazar un parangón entre las esencias de MetaTrader 5 y MetaTrader 4. Será más fácil empezar con la versión 4. Ella contiene un concepto universal «orden», que se utiliza prácticamente para todo: para las órdenes de mercado, órdenes pendientes, historial de operaciones comerciales. En todas estas situaciones, la orden se encuentra en estados diferentes. En la versión 5, las órdenes de mercado representan las posiciones, las órdenes pendientes son simplemente las órdenes, mientras que el historial de operaciones se registra a través de las transacciones.

En el caso más simple, MetaTrader 5 funciona aproximadamente así. Para la formación de una posición, al servidor comercial se le envía una orden de entrada en el mercado. Para cerrar una posición, se recibe otra orden para salir del mercado. La ejecución de cada una de las órdenes se realiza de acuerdo con la transacción correspondiente, que a su vez se registra en el historial del trading. De esta manera, una orden de la versión 4 debe visualizarse en el entorno comercial emulado de la versión 5 como sigue:

  • orden de entrada
  • transacción de salida
  • posición
  • orden de salida
  • transacción de salida

Recordemos que originalmente MetaTrader 5 era una plataforma estrictamente de compensación (netting), es decir, para un símbolo podía existir simultáneamente sólo una posición. Todas las órdenes para un símbolo aumentaban, reducían o eliminaban completamente el volumen total para el símbolo, así como cambiaban para él los niveles generales de Stop Loss y Tahke Profit. Este modo no existe en MetaTrader 4, y por tanto tendríamos que hacer un gran esfuerzo para plasmar este proyecto si en MetaTrader 5 no apareciera el soporte de la cobertura (hedging). MetaTrader 4 profesa precisamente este modo: la ejecución de cada orden forma una «posición» separada (en los términos de MetaTrader 5), así que para el mismo símbolo pueden haber varias órdenes abiertas, inclusive las órdenes bidireccionales.

¡Atención! Si Usted quiere comparar el trabajo de los EAs generados en MetaTrader 5 y en MetaTrader 4, no olvide de que en MetaTrader 5 debe tener activado el tipo de la cuenta con cobertura (hedging). Para la comparación, es deseable usar el servidor del mismo bróker.

Implementación

Emulación del entorno comercial de MetaTrader 5

Para simplificar, colocaremos todo el entorno emulado (inclusive los tipos, constantes y funciones) en el único archivo de cabecera MT5Bridge.mqh. Probablemente, un buen estilo de programación requeriría su distribución por los archivos separados. Esta estructuración es especialmente importante para unos proyectos grandes y proyectos desarrollados por un grupo de personas. No obstante, desde el punto de vista de distribución e instalación, un archivo es más conveniente.

Pues bien, de acuerdo con el plano, vamos a definir todas las enumeraciones, constantes y estructuras. Es un trabajo rutinario del copiado, que no supone ningunas dificultades. Quizá, no merece la pena explicarlo más detalladamente, aparte de los comentarios que ya han sido dados durante el planeamiento. Luego, echamos de nuevo un vistazo a la página de la documentación sobre las Funciones comerciales, y comenzaremos con la parte más intelectual, la escritura del código de todas estas funciones.

Empezaremos con las operaciones actuales que incluyen el procesamiento de las órdenes pendientes y de mercado, así como las posiciones.

Para eso, necesitaremos la función superuniversal OrderSend de la quinta versión.

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

Ahí, dependiendo del tipo de la solicitud, es necesario usar uno de los tipos de las órdenes de MetaTrader 4.

  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;
  }

El código de la operación pasado al campo action permite procesar de diferentes formas la instalación, eliminación y modificación de órdenes. Por ejemplo, la apertura de una orden de mercado o la creación de una orden pendiente puede ser implementada de la siguiente manera.

  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 = "";
        }
      }
    }
  }

El trabajo principal se realiza por la función habitual para MetaTrader 4, OrderSend, con una gran variedad de parámetros. Después de ser llamada, los resultados del trabajo se registran de forma correspondiente en la estructura de salida.

Destacamos que en MetaTrader 5 el cierre de la orden de mercado existente se realiza mediante la apertura de otra orden opuesta, y el identificador de la posición a cerrar se pasa al campo position. En este caso, o sea cuando el campo position no está vacío, el código de arriba intenta cerrar la orden usando la función OrderClose. El ticket de la misma orden se utiliza como el identificador de la posición. Es lógico, porque en la versión 4, cada orden crea su propia posición. La transacción recibe el mismo ticket.

Lo que se refiere a la orden virtual del cierre de la posición (en realidad, aquí no existe), como su ticket se utiliza de manera virtual el número original completado con el bit mayor establecido a 1. Eso va a aplicarse luego para la enumeración de órdenes y transacciones.

Ahora veremos como se puede implementar el cambio de los niveles en una posición abierta.

  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;
    }
  }

Es obvio que para eso se utiliza OrderModify.

La misma función se utiliza también para cambiar una orden pendiente.

  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;
    }
  }

De la eliminación de una orden pendiente se encarga la función estándar OrderDelete.

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

Finalmente, el cierre de una posición usando otra (opuesta) es equivalente —en el contexto de MetaTrader 4— al cierre de las órdenes opuestas.

  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;
}

Aparte de OrderSend, MetaTrader 5 presenta la función asincrónica OrderSendAsync. Nosotros no vamos a implementarla: desactivamos todos los casos de la aplicación del modo asincrónico en la librería, o sea, en realidad, reemplazamos por la versión sincrónica.

A menudo, la colocación de las órdenes de trabajo, se acompaña con las llamadas a 3 otras funciones: OrderCalcMargin, OrderCalcProfit, OrderCheck.‌

Aquí tenemos una de las variantes de su implementación usando los recursos disponibles en 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; }

Aquí se utiliza activamente las funciones incorporadas AccountEquity, AccountFreeMargin, AccountFreeMarginCheck, así como el coste del punto del instrumento y otras configuraciones obtenidas llamando a MarketInfo.

Para obtener el número total de posiciones, será suficiente devolver el número de órdenes de mercado abiertas.

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;
}

Para obtener el símbolo de la posición según su número, es necesario recorrer en el ciclo todas las órdenes, contando sólo las de mercado.

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 "";
}

De la misma manera, se construye la función para obtener el ticket de la posición según su número.

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;
}

Para seleccionar la posición según el nombre del símbolo, también recorremos en el ciclo las órdenes de mercado y detenemos en la primera que coincide por el símbolo.

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;
}

La implementación de la selección de la posición según el ticket no requiere el ciclo.

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

Las propiedades de la posición seleccionada deben ser devueltas por tres funciones habituales para MetaTrader 5: _GetDouble, _GetInteger, _GetString. Aquí presentamos su implementación para las posiciones. En cuanto a las órdenes y transacciones, estas funciones serán muy parecidas, y por eso se quedarán fuera del presente artículo. El que quiere puede estudiar su código en el archivo adjunto.

// posición = orden, sólo OP_BUY o 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;
}

De la misma manera, como en el caso de las posiciones que representan las órdenes de mercado, hay que implementar el conjunto de funciones para el procesamiento de las órdenes pendientes. Pero aquí hay un problema. No podemos implementar la función OrdersTotal y otras OrderGet_, porque ya están definidas en el núcleo, y la redefinición de las funciones incorporadas es imposible. El compilador genera el siguiente error:

'OrderGetString' - override system function MT5Bridge.mqh

Por eso, nos vemos obligados a renombrar todas las funciones que llevan los nombres con el prefijo Order_. Sería lógico comenzar sus nombres con PendingOrder_, ya que procesan exclusivamente las órdenes pendientes. Por ejemplo:

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;
}

Luego, en el código de la librería estándar, habrá que reemplazar todas las llamadas por nuestras nuevas funciones desde MT5Bridge.mqh.

La función OrderGetTicket, que devuelve el ticket de la orden según el número, no existe en MetaTrader 4, por eso dejamos su nombre tal como es, o sea, de acuerdo con API de MetaTrader 5.‌

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;
}

La función OrderSelect existe en MetaTrader 4 con la lista expandida de parámetros en comparación con MetaTrader 5, por eso dejaremos sus llamadas, completándolas con el parámetro necesario SELECT_BY_TICKET.

La implementación completa de las funciones de lectura de las propiedades de órdenes pendientes se encuentra en el archivo de cabecera que se adjunta.

Ahora pasaremos a las funciones para el trabajo con el historial de las órdenes y transacciones. Su implementación exige engenio. La versión que se muestra a continuación es sólo una de muchas opciones posibles, y ha sido escogida debido a su simplicidad.

Cada orden de mercado de MetaTrader 4 se muestra en el historial por medio de «dos órdenes a la MetaTrader 5»: de entrada y de salida. Además de eso, el historial debe contener el par de transacciones correspondiente. Las órdenes pendientes se muestran sin cambios. El historial va a almacenarse en dos arrays con tickets.

int historyDeals[], historyOrders[];

Van a llenarse por la función HistorySelect de 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;
}

Una vez llenados los arrays, se puede obtener el tamaño del historial.

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

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

Los tamaños de los arrays se multiplican por 2, porque cada orden de MetaTrader 4 representa dos órdenes o dos transacciones de MetaTrader 5. Para las órdenes pendientes no es así, pero con el fin de guardar la comunidad del enfoque, nosotros igualmente reservamos 2 tickets, pero uno de ellos no va a usarse (ver abajo la función HistoryOrderGetTicket). La transacción de entrada en el mercado va a recibir el mismo ticket que tiene la orden de MetaTrader 4 que la genera. Y para la transacción de salida, vamos a completar este ticket con un bit mayor.

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;
}

En el historial, los números pares siempre suponen los tickets de entrada (reales), los impares, de salida (virtuales).

En cuanto a las órdenes, todo es un poco más complicado, ya que entre ellas, pueden encontrarse las órdenes pendientes que se muestran de la misma manera. En este caso, el número par devolverá el ticket correcto de la orden pendiente, y el impar siguiente, 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;
}

La selección de la transacción según el ticket se implementa tomando en cuenta esta particularidad con la definición del bit mayor— aquí hay que reseteralo.

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

Para la orden, todo es completamente lo mismo.

#define HistoryOrderSelect HistoryDealSelect

Teniendo la transacción seleccionada a través de HistoryDealSelect o HistoryDealGetTicket, se puede escribir la implementación de las funciones de acceso a las propiedades de la transacción.

#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;
}

Espero que la idea esté clara. El grupo de funciones para el trabajo con las órdenes en el historial se implementa de la misma manera.

Cambios en los archivos de la librería estándar‌

Algunas correcciones necesarias de la librería ya fueron discutidas durante la implementación de las funciones. Usted mismo puede comparar los archivos de la entrega de MetaTrader 5 con los que hemos obtenido en este proyecto, para conseguir la lista completa de modificaciones. A continuación, se consideran sólo los momentos más importantes, mientras que los comentarios relacionados con las correcciones pequeñas se omiten. Muchos archivos han incluido la directiva nueva #include para la inclusión de MT5Bridge.mqh.

Tabla de los cambios principales en los archivos de la librería estándar


Archivo/Método Cambios
Trade.mqh SetAsyncMode ha sido eliminada la línea que aplica el modo asincrónico ya que este modo no se soporta
SetMarginMode ha sido escrito explícitamente el modo ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
OrderOpen ha sido escrita explícitamente la combinación de banderas que establecen el modo de expiración como SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED
OrderTypeCheck han sido excluidos los casos de procesamiento de tipos no existentes ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT
OrderSend ha sido eliminada la llamada a la función asincrónica ausente OrderSendAsync
 
OrderInfo.mqh todas las llamadas de las funciones OrderGetInteger, OrderGetDouble, OrderGetString han sido sustituidas por las funciones homónimas con el prefijo PendingOrder
todas las llamadas de OrderSelect(m_ticket) han sido sustituidas por OrderSelect((int)m_ticket, SELECT_BY_TICKET)
 
PositionInfo.mqh FormatPosition
SelectByIndex
ha sido definido el modo del margen ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
 
SymbolInfo.mqh Refresh han sido excluidas muchas comprobaciones no soportadas en MetaTrader 4
 
AccountInfo.mqh MarginMode devuelve la constante ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
 
Expert.mqh TimeframeAdd
TimeframesFlags
han sido eliminados los períodos de tiempo no soportados
 
ExpertBase.mqh añadido #include <Indicators\IndicatorsExt.mqh>
SetMarginMode ha sido establecido definitivamente en ACCOUNT_MARGIN_MODE_RETAIL_HEDGING


El archivo IndicatorsExt.mqh es necesario para corregir pequeños errores en el archivo Indicators.mqh. Además de eso, incluye otro archivo de cabecera necesario para los indicadores, TimeSeriesExt.mqh.‌

El archivo TimeSeriesExt.mqh contiene las definiciones de las clases necesarias para «tradear a la MetaTrader 5», pero no existen en el archivo TimeSeries.mqh proporcionado con MetaTrader 4.

En particular, son las siguientes clases: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume. Muchas de ellas son simplemente los «tapones» que no hacen nada (y no pueden hacer debido a la inaccesibilidad de la funcionalidad en MetaTrader 4).

Testeo

Después de colocar las clases comerciales adoptadas de la librería estándar en el directorio Include de MetaTrader 4 (guardando la jerarquía de subcarpetas), así como después de copiar MT5Bridge.mqh a la carpeta Include/Trade, podemos compilar y ejecutar los Asesores Expertos generados por el Asistente de MetaTrader 5 directamente en MetaTrader 4.

La entrega de MetaTrader 5 incluye varios ejemplos de los EAs generados (en la carpeta Experts/Advisors). Usaremos uno de ellos, ExpertMACD.mq5. Lo copiamos a la carpeta MQL4/Experts y cambiamos su nombre por ExpertMACD.mq4. La compilación en el editor debe dar aproximadamente el siguiente resultado:

El Asesor Experto del Asistente de MetaTrader 5 compilado en MetaTrader 4

El Asesor Experto del Asistente de MetaTrader 5 compilado en MetaTrader 4

Como podemos ver, los archivos de la librería están conectados y se procesan sin errores y avisos. Claro que la ausencia de los errores de compilación no garantiza la ausencia de problemas en la lógica del programa, pero eso será el objeto de las siguientes comprobaciones en la práctica.

Vamos a iniciar el EA compilado con ajustes predefinidos en el Probador de Estrategias de MetaTrader 4.

Informe del testeo de MetaTrader 4 para el EA generado en MetaTrader 5

Informe del testeo de MetaTrader 4 para el EA generado en MetaTrader 5

Si desea, puede asegurarse de que en el registro no hay ningún error explícito del procesamiento de las órdenes.

En el gráfico EURUSD M15, el trading de este EA parece estar bien, incluyendo, en particular, el establecimiento de los niveles Stop Loss y Take Profit.

Ventana del gráfico que muestra el trabajo del Asesor Experto del Asistente de MetaTrader 5 en MetaTrader 4

Ventana del gráfico que muestra el trabajo del Asesor Experto del Asistente de MetaTrader 5 en MetaTrader 4

Comparamos con los resultados del Probador de MetaTrader 5.

Informe del testeo de MetaTrader 5 para el EA generado

Informe del testeo de MetaTrader 5 para el EA generado

Es evidente que hay diferencias. Ellas pueden explicarse con las divergencias tanto en las propias cotizaciones (por ejemplo, MetaTrader 5 utiliza el spread flotante), como en los algoritmos del Probador. En general, las pruebas son parecidas: aproximadamente, el número de operaciones y el carácter general de la curva del balance coinciden.

Desde luego, el usuario puede generar en el Asistente su propio EA con un conjunto de módulos completamente arbitrario, y él debe traspasarse a MetaTrader 4 con facilidad. Durante el testeo del proyecto, fueron verificados, en particular, los EAs con el Trailing y el tamaño variable del lote.

Conclusión

Pues, hemos considerado una de las posible formas del traspaso de los Asesores Expertos generados por el Asistente de MetaTrader 5 a la plataforma MetaTrader 4. Su ventaja principal es una relativa facilidad de la implementación, basada en el uso máximo del código existente de las clases comerciales desde la librería estándar de MetaTrader 5. El inconveniente principal es la necesidad de tener instalados ambos terminales en el ordenador: uno para generar los EAs, otro para usarlos.

Abajo se adjuntan 2 archivos:

  • archivo comprimido con ficheros modificados de la librería estándar que es necesario descomprimir, guardando la jerarquía de las subcarpetas, en el directorio Include de MetaTrader 4. El archivo contiene sólo los ficheros que no entran en la entrega del terminal, por eso no hay peligro de sobrescribir los ficheros existentes;
  • archivo MT5Bridge.mqh, que hay que copiar a la carpeta Include/Trade.

Esta versión de la librería ha sido cogida desde la compilación 1545 de MetaTrader 5. Las futuras compilaciones pueden incluir los cambios en la librería estándar que pueden ser útiles (lo que exigirá ejecutar de nuevo la fusión con las correcciones del emulador). Sería ideal ver algún día la versión de la librería estándar de MetaQuotes, en la que a través de las directivas de compilación condicional se combinen desde el principio dos opciones de la implementación de las clases comerciales: para MetaTrader 5 y para MetaTrader 4.

Cabe mencionar que será imposible implementar la emulación completa del entorno comercial de MetaTrader 5 en MetaTrader 4. El nuevo terminal facilita nuevos recursos que no están presentes en el anterior a nivel del núcleo. Por eso, es muy probable que algunos módulos utilizados en los EAs generados se nieguen a trabajar.

Además, no hay que olvidar que la presente implementación del emulador se distribuye como versión beta y puede contener errores ocultos. Sólo un testeo multifacético y de larga duración permitirá conseguir un producto final adecuado para el trading. La disponibilidad de los códigos fuente permite hacer eso conjuntamente.