Uso de MetaTrader 5 como proveedor de señales comerciales para MetaTrader 4

Karlis Balcers | 7 marzo, 2014

Introducción

Tenía varias razones para estudiar esta cuestión y escribir este artículo.

En primer lugar, a pesar de que el lanzamiento oficial de la plataforma MetaTrader 5 haya tenido lugar hace bastante tiempo, todavía seguimos esperando a que nuestros brokers nos permitan operar en las cuentas reales. Algunos ya han conseguido realizar las estrategias acertadas en el lenguaje MQL5 y les gustaría usarlas en las cuentas reales. A otros, a lo mejor, les atrae más la organización del trading en MetaTrader 5, y les gustaría operar manualmente en esta plataforma en vez de usar MetaTrader 4.

En segundo lugar, en el transcurso de Automated Trading Championship a muchas personas se les ha ocurrido la idea de copiar las transacciones de los líderes a sus cuentas reales. Algunos traders han creado sus propios modos de copiar las transacciones, otros todavía siguen buscando las mejores opciones de implementar esta idea (y maneras de administrar el capital) con el objetivo de obtener los resultados más cercanos a los participantes del Campeonato.

En tercer lugar, algunos traders tienen buenas estrategias y les gustaría proveer sus señales comerciales. Ellos necesitan la posibilidad de distribuir sus señales comerciales a varias cuentas en tiempo real sin perder la rentabilidad.

Estas cuestiones me interesaban siempre, y ahora voy a intentar encontrar una solución que satisfaga todos estos requerimientos.


1. ¿Cómo copiar el trading de los participantes de Automated Trading Championship?

Últimamente he encontrado varios artículos en el sitio web MQL5.community que he llegado a entender y que me han llevado a la idea de que soy capaz de hacerlo. Además, les diré que tenía un programa que utilizaba para tradear en la cuenta real (afortunadamente, con beneficios) siguiendo las transacciones de los participantes que se publicaban en la página del Campeonato. El problema consistía en que los datos se actualizaban cada 5 minutos y había la posibilidad de pasarse del momento de apertura y cierre de transacciones.

En el foro del Campeonato me he enterrado de que otras personas también utilizan este método. Sin embargo, no es eficaz. Además, crea un enorme tráfico en el servidor del Campeonato y a los organizadores puede que no les guste todo eso. Por tanto, ¿hay alguna solución? He repasado todas las opciones y me ha gustado la posibilidad de acceder a la cuenta de cualquier participante usando la contraseña de inversor (al usarla el trading queda prohíbido) en MetaTrader 5.

¿Podemos usar este método para recibir la información de toda la actividad comercial y transmitirla en tiempo real? Para comprobarlo, he creado un Asesor Experto y he intentado iniciarlo en la cuenta disponible sólo a través de la contraseña de inversor. Para mi sorpresa me ha salido iniciarlo y ha sido posible recibir la información sobre las posiciones, órdenes y transacciones, ¡Ésta era la puerta hacia la posible solución!


2. ¿Qué copiamos: posiciones, órdenes o transacciones?

Si tendremos que pasar la información desde MetaTrader 5 a MetaTrader 4, tendremos que tener en consideración todos los tipos de órdenes que son posibles en MetaTrader 4. Además de eso, cuando seguimos el trading, queremos estar informados sobre cualquier acción que se realiza en la cuenta de trading. La comparación del estatus de posiciones (Positions) en cada tick o cada segundo no nos ofrece la información completa.

Por esta razón, es mejor seguir las órdenes (Orders) o transacciones (Deals).

Me he puesto a mirar la estructura de las órdenes.

Orders

Me han gustado las órdenes porque se colocan antes de que se ejecute la transacción, y además contienen la información sobre si se trata de una orden pendiente o no. Sin embargo, en las órdenes falta una cosa importante que está presente en las transacciones: es el tipo de la transacción (ENUM_DEAL_ENTRY):

Deals

El tipo de la transacción DEAL_ENTRY_TYPE ayuda a comprender qué es lo que ha pasado con la cuenta del trader, mientras que el trabajo con las órdenes requiere hacer cálculos. Sería mejor trabajar con las órdenes y las transacciones a la vez. Eso nos daría la posibilidad de seguir exactamente el trading en los casos cuando se utilizan las órdenes pendientes. Puesto que diferentes brokers pueden tener el carácter de movimiento de precios diferente, el uso de las órdenes pendientes puede provocar los errores y resultados incorrectos.

En caso cuando seguimos sólo las transacciones (Deals), seguiremos ejecutando las órdenes pero con un pequeño retraso que se determina por la conexión de red. Al elegir entre la velocidad (órdenes pendientes) y la rentabilidad (transacciones), he elegido lo último.


3. ¿Cómo proveer las "señales"?

Los modos de conexión y transmisión de datos entre el terminal MetaTrader y otras aplicaciones y ordenadores ya han sido discutidos en diferentes artículos. Puesto que yo quiero que otros clientes tengan la posibilidad de conectarse con nosotros (seguramente van a usar otros ordenadores para ello), he elegido la conexión a través de los sockets usando el protocolo TCP.

Ya que el lenguaje MQL5 no permite trabajar directamente con las funciones API, tendremos que usar las bibliotecas externas. Hay varios artículos sobre cómo usar la biblioteca "WinInet.dll" (por ejemplo, "Uso de WinInet.dll para el intercambio de datos entre los terminales vía Internet", y los demás), pero ninguno de ellos satisface nuestros requerimientos.

Ya que estoy familiarizado un poco con el lenguaje C# (antes he hecho unos servidores que trabajan en tiempo real), he decidido crear mi propia biblioteca. He usado el artículo "Descubriendo el mundo de C# desde MQL5 mediante la exportación del código no manejado" que me ha ayudado solucionar los problemas de compatibilidad. He escrito el servidor con una interfaz muy sencilla y la posibilidad de conectarse simultáneamente con 500 clientes (para su trabajo tiene que tener instalado en su ordenador .NET framework de la versión 3.5 o superior, la mayoría de los ordenadores ya tiene instalado "Microsoft .NET Framework 3.5).

#import "SocketServer.dll"    // Biblioteca escrita en el lenguaje C# 
                              // (creada según el método descrito en el artículo https://www.mql5.com/es/articles/249)
string About();            // Información sobre la biblioteca.
int SendToAll(string msg);  // Envía un mensaje de texto a todos los clientes.
bool Stop();               // Para el servidor.
bool StartListen(int port); // Arranca el servidor. El servidor va a escuchar todas las conexiones de entrada (hasta 500 conexiones). 
                               // Todos los clientes están construidos a base de los Flujos asincrónicos.
string ReadLogLine();       // Solicita una línea del log del servidor (puede contener la información sobre los errores y otros datos). 
                               // El servidor guarda sólo las últimas 100 líneas del log.
#import

El servidor de por sí trabaja en el modo subordinado (background mode) en diferentes flujos. Por eso el trabajo del terminal o la estrategia no se ralentiza y no se bloquea, sin importar el número de los clientes conectados.

El código en C#:

internal static void WaitForClients()
        {
            if (server != null)
            {
                Debug("Cant start lisening! Server not disposed.");
                return;
            }
            try
            {

                IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, iPort);
                server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                server.Bind(localEndPoint);
                server.Listen(500);
                isServerClosed = false;
                isServerClosedOrClosing = false;

                while (!isServerClosedOrClosing)
                {
                    allDone.Reset();
                    server.BeginAccept(new AsyncCallback(AcceptCallback), server);
                    allDone.WaitOne();
                }
                
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception e)
            {
                Debug("WaitForClients() Error: " + e.Message);
            }
            finally
            {
                if (server != null)
                {
                    server.Close();
                    server = null;
                }
                isServerClosed = true;
                isServerClosedOrClosing = true;
            }
        }

        internal static void AcceptCallback(IAsyncResult ar)
        {
            try
            {
                allDone.Set();
                if (isServerClosedOrClosing)
                    return;
                Socket listener = (Socket)ar.AsyncState;
                Socket client = listener.EndAccept(ar);

                if (clients != null)
                {
                    lock (clients)
                    {
                        Array.Resize(ref clients, clients.Length + 1);
                        clients[clients.Length - 1].socket = client;
                        clients[clients.Length - 1].ip = client.RemoteEndPoint.ToString();
                        clients[clients.Length - 1].alive = true;
                    }
                    Debug("Client connected: " + clients[clients.Length - 1].ip);
                }
            }
            catch (Exception ex)
            {
                Debug("AcceptCallback() Error: " + ex.Message);
            }
        }

Para más detalles sobre el trabajo de los Sockets Asincrónicos de Servidor C#, lea Microsoft MSDN o los artículos que se puede encontrar usando Google.


4. Envío de señales comerciales

En el terminal MetaTrader 4 nos gustaría recibir la información constantemente (no sólo cuando llega nuevo tick), pare este propósito en vez del Asesor Experto vamos a utilizar el script. Además, necesitaremos tener la posibilidad de abrir una conexión de socket con nuestro proveedor de señales comerciales: terminal MetaTrader 5.

Para eso he utilizado MQL4 codebase: "https://www.mql5.com/es/code/9296" donde se encuentra una buena biblioteca (WinSock.mqh) que permite trabajar fácilmente con los sockets. Algunos se han quejado de la estabilidad de su funcionamiento. Sin embargo, me ha valido para mis objetivos, y durante la simulación no me ha surgido problema alguno.

#include <winsock.mqh>  // Biblioteca de Codebase en mql4.com
                        // Enlace:   https://www.mql5.com/es/code/9296
                        // Artículo: https://www.mql5.com/es/code/download/9296


5. Procesamiento de datos

Ahora, cuando ya tenemos claro el concepto de trabajo, nos hace falta realizar el envío de la información sobre las transacciones a todos los clientes en el formato que puedan comprender y procesar.

5.1. Lado del Servidor

Como ya hemos aclarado antes, será el Asesor Experto. Pero él no tiene que estar vinculado al par de divisas del gráfico.

Durante el arranque, éste inicia el flujo de escucha que va a esperar las conexiones entrantes:

int OnInit()
  {
   string str="";
   Print(UTF8_to_ASCII(About()));
//--- inicio del servidor
   Print("Starting server on port ",InpPort,"...");
   if(!StartListen(InpPort))
     {
      PrintLogs();
      Print("OnInit() - FAILED");
      return -1;
     }

En esta versión al Asesor Experto no le importa si los clientes están conectados o no. En cuanto se ejecute cada transacción, va a enviar las notificaciones a todos los clientes, incluso si no están. Ya que necesitamos sólo la información sobre las transacciones, vamos a utilizar la función OnTrade() y vamos a quitar el manejador OnTick(). En esta función vamos a seguir el historial de trading, y cuando se ejecute una transacción, decidimos si enviamos la información o no.

Preste atención a mis comentarios para entender mejor el código:

//+------------------------------------------------------------------+
//| OnTrade() - manejador de eventos de la actividad comercial       |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- encontraremos todas nuevas transacciones e informaremos a los clientes de ellas
//--- -24 horas
   datetime dtStart=TimeCurrent()-60*60*24;
//--- +24 horas (si su hora-<hours>)
   datetime dtEnd=TimeCurrent()+60*60*24;
//--- solicitamos el historial de las últimas 24 horas
   if(HistorySelect(dtStart,dtEnd))
     {
      //--- repasamos todas las transacciones (de las antiguas a las recientes)
      for(int i=0;i<HistoryDealsTotal();i++)
        {
         //--- obtenemos el ticket de la transacción
         ulong ticket=HistoryDealGetTicket(i);
         //--- if this deal is interesting for us.
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)!=DEAL_ENTRY_STATE)
           {
            //Print("Entry type ok.");
            //--- comprobamos si fue esta transacción más antigua de la que avisamos la última vez
            if(HistoryDealGetInteger(ticket,DEAL_TIME)>g_dtLastDealTime)
              {
               //--- si una parte de la posición ha sido cerrada, comprobamos la necesidad de permitir el símbolo
               if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
                 {
                  vUpdateEnabledSymbols();
                 }
               //--- si la posición está abierta en dirección contraria, hay que habilitar el símbolo deshabilitado
               else if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
                 {
                  //--- habilitamos este símbolo
                  vEnableSymbol(HistoryDealGetString(ticket,DEAL_SYMBOL));
                 }
               //--- comprobar el permiso de tradear con este símbolo
               if(bIsThisSymbolEnabled(HistoryDealGetString(ticket,DEAL_SYMBOL)))
                 {
                  //--- preparación de la línea con la transacción y su envío a todos los clientes conectados
                  int cnt=SendToAll(sBuildDealString(ticket));
                  //--- problema técnico en el servidor.
                  if(cnt<0)
                    {
                     Print("Failed to send new deals!");
                    }
                  //--- si no se ha enviado a nadie (cnt==0) o se ha enviado a alguien (cnt>0)                  
                  else
                    {
                     //--- actualizamos la hora de la última transacción
                     g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                    }
                 }
               //--- no hace falta la notificación, ya el trading con el símbolo está prohibido
               else
                 {
                  //--- actualizamos la hora de la última transacción de la que no vamos a avisar
                  g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                 }
              }
           }
        }
     }
  }

Como ya ha entendido, si encontramos nueva transacción, llamamos a la función BuildDealString() para preparar los datos para el envío. Todos los datos se envían en el formato de texto, la información sobre cada transacción se empieza con el símbolo '<' y se termina con el símbolo  '>'.

Eso nos ayudará separar las líneas con varias transacciones, ya que durante el envío de datos a través del protocolo TCP/IP los paquetes pueden unirse pudiendo contener la información sobre varias transacciones.

//+------------------------------------------------------------------+
//| Función para construir la línea con la información               |
//| sobre la transacción                                             |
//| Ejemplos:                                                        |
//| EURUSD;BUY;IN;0.01;1.37294                                       |
//| EURUSD;SELL;OUT;0.01;1.37310                                     |
//| EURUSD;SELL;IN;0.01;1.37320                                      |
//| EURUSD;BUY;INOUT;0.02;1.37294                                    |
//+------------------------------------------------------------------+
string sBuildDealString(ulong ticket)
  {
   string deal="";
   double volume=0;
   bool bFirstInOut=true;
//--- determinamos el volumen de la transacción
//--- caso cuando la transacción es INOUT
   if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
     {
      if(PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
        {
         volume=PositionGetDouble(POSITION_VOLUME);
        }
      else
        {
         Print("Failed to get volume!");
        }
     }
//--- si la transacción es 'IN' o 'OUT', usamos el volumen de la transacción
   else
     {
      volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
     }
//--- construimos la línea de la transacción (ejemplo: "<EURUSD;BUY;IN;0.01;1.37294>").
   int iDealEntry=(int)HistoryDealGetInteger(ticket,DEAL_ENTRY);
//--- si es la transacción OUT y no hay posiciones abiertas
   if(iDealEntry==DEAL_ENTRY_OUT && !PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
     {
      //--- Por razones de seguridad, comprobamos si ha quedado alguna posición con el símbolo actual. Si NO, vamos a
      //--- usar nuevo tipo de la transacción - OUTALL. Eso nos garantiza que después de eso no quedarán órdenes abiertas
      //--- y la posición será cerrada en los clientes "remotos". Eso se debe a las particularidades de visualización de los volúmenes
      //--- en los nuevos valores en el lado del cliente, por eso la diferencia puede ser muy pequeña entre ellos, y la orden
      //--- con un volumen muy pequeño puede quedarse.
      //--- valor introducido por mí (en la enumeración ENUM_DEAL_ENTRY no viene).
      iDealEntry=DEAL_ENTRY_OUTALL;  
     }
   StringConcatenate(deal,"<",AccountInfoInteger(ACCOUNT_LOGIN),";", HistoryDealGetString(ticket,DEAL_SYMBOL),";",
                   Type2String((ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE)),";",
                   Entry2String(iDealEntry),";",DoubleToString(volume,2),";",
                   DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE),
                   (int)SymbolInfoInteger(HistoryDealGetString(ticket,DEAL_SYMBOL),SYMBOL_DIGITS)),">");
   Print("DEAL:",deal);
   return deal;
  }

Tal vez esté sorprendido por aparición del nuevo tipo de transacción - DEAL_ENTRY_OUTALL. Este tipo ha sido creado por mí. La razón de su aparición la va a entender cuando les explique el manejo de los volúmenes de trading en el lado de MetaTrader 4.

Otro momento interesante es la función OnTimer(). Durante la inicialización se realiza la llamada a la función EventSetTimer(1) para llamar OnTimer() cada segundo. El código del manejador del temporizador contiene sólo una línea que muestra la información en los logs del servidor:

//+------------------------------------------------------------------+
//| Muestra los logs del servidor cada segundo (si existen)          |
//+------------------------------------------------------------------+
void OnTimer()
  {
   PrintLogs();
  }

Para mostrar la información sobre el estatus y los errores, yo recomiendo llamar a la función PrintLogs() tras la ejecución de cada función de la biblioteca de servidor.

En el lado del servidor también encontrará el parámetro de entrada StartupType:

//+------------------------------------------------------------------+
//| Modo de inicio                                                   |
//+------------------------------------------------------------------+
enum ENUM_STARTUP_TYPE
  {
   STARTUP_TYPE_CLEAR,    // CLEAR - enviar cada nueva TRANSACCIÓN que aparece en la cuenta 
                          // (Send every new DEAL wich appears on account).
   STARTUP_TYPE_CONTINUE  // CONTINUE - no enviar la TRANSACCIÓN hasta el cierre de la POSICIÓN actual
                          // (Do not send DEAL before existing POSITION has been closed).
  };
//--- parámetros de entrada
input int               InpPort=2011;                         // Puerto de entrada
input ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; // Tipo del inicio

Este parámetro ha sido añadido porque el Asesor Experto (Proveedor de Señales) puede ser agregado a la cuenta que ya tiene las posiciones abiertas (por ejemplo, abiertas durante el Campeonato) y por eso dicha información puede se confusa para el lado del cliente. Este parámetro ayuda elegir el modo de recepción de la información: las posiciones abiertas existentes o sólo nuevas posiciones.

También es importante si Usted se conecta por primera vez a la cuenta en la que se realiza el trading, o se ha conectado antes pero ha reiniciado el ordenador, programa o ha realizado la modificación de su código.


5.2. Lado del Cliente

En la parte del cliente tenemos un script con el ciclo infinito y que contiene la función de recepción de datos del socket recv. Puesto que esta función no es de bloqueo (hasta el momento de recepción de los datos del servidor), no produce una carga significante del tiempo de procesador. 

/--- arranque e inicio de la recogida y procesamiento de datos
   while(!IsStopped())
     {
      Print("Client: Waiting for DEAL...");
      ArrayInitialize(iBuffer,0);
      iRetVal=recv(iSocketHandle,iBuffer,ArraySize(iBuffer)<<2,0);
      if(iRetVal>0)
        {
         string sRawData=struct2str(iBuffer,iRetVal<<18);
         Print("Received("+iRetVal+"): "+sRawData);

Pero eso provoca un problema si el cliente se para. Al llamar el comando de detención del script (Remove Script), éste no se para a la primera. Hay que hacerlo dos veces y el script se detendrá por el tiempo de inactividad (time-out). Se puede solucionarlo instalando el time-out para la función recv, pero como se utiliza la biblioteca de libre acceso de Codebase dejaremos esta tarea para su autor.

Después de obtener los datos, los procesamos y comprobamos hasta la ejecución de la transacción en la cuenta real:

         //--- análisis sintáctico de entradas
         string arrDeals[];
         //--- dividimos los datos en varias transacciones (si son varias)
         int iDealsReceived=Split(sRawData,"<",10,arrDeals);
         Print("Found ",iDealsReceived," deal orders.");
         //--- procesamiento de cada transacción
         //--- ciclo por todas las transacciones obtenidas
         for(int j=0;j<iDealsReceived;j++) 
           {
            //--- analizamos el valor de cada entrada de la transacción
            string arrValues[];
            //--- obtenemos los valores
            int iValuesInDeal=Split(arrDeals[j],";",10,arrValues);
            //--- comprobamos la corrección del formato de datos obtenidos (cantidad de datos)
            if(iValuesInDeal==6)
              {
               if(ProcessOrderRaw(arrValues[0],arrValues[1],arrValues[2],arrValues[3],
                                  arrValues[4],StringSubstr(arrValues[5],0,StringLen(arrValues[5])-1)))
                 {
                  Print("Processing of order done sucessfully.");
                 }
               else
                 {
                  Print("Processing of order failed:\"",arrDeals[j],"\"");
                 }
              }
            else
              {
               Print("Invalid order received:\"",arrDeals[j],"\"");
               //--- this was last one in array
               if(j==iDealsReceived-1)
                 {
                  //--- el inicio de la siguiente transacción puede ser incompleto
                  sLeftOver=arrDeals[j];
                 }
              }
           }
//+------------------------------------------------------------------+
//| Procesamiento de datos "crudos" (raw) recibidos                  |
//| (en el formato de texto)                                         |
//+------------------------------------------------------------------+
bool ProcessOrderRaw(string saccount,string ssymbol,string stype,
                    string sentry,string svolume,string sprice)
  {
//--- borrar
   saccount= Trim(saccount);
   ssymbol = Trim(ssymbol);
   stype=Trim(stype);
   sentry=Trim(sentry);
   svolume= Trim(svolume);
   sprice = Trim(sprice);
//--- comprobación de corrección
   if(!ValidateAccountNumber(saccount)){Print("Invalid account:",saccount);return(false);}
   if(!ValidateSymbol(ssymbol)){Print("Invalid symbol:",ssymbol);return(false);}
   if(!ValidateType(stype)){Print("Invalid type:",stype);return(false);}
   if(!ValidateEntry(sentry)){Print("Invalid entry:",sentry);return(false);}
   if(!ValidateVolume(svolume)){Print("Invalid volume:",svolume);return(false);}
   if(!ValidatePrice(sprice)){Print("Invalid price:",sprice);return(false);}
//--- conversiones
   int account=StrToInteger(saccount);
   string symbol=ssymbol;
   int type=String2Type(stype);
   int entry=String2Entry(sentry);
   double volume= GetLotSize(StrToDouble(svolume),symbol);
   double price = NormalizeDouble(StrToDouble(sprice),(int)MarketInfo(ssymbol,MODE_DIGITS));
   Print("DEAL[",account,"|",symbol,"|",Type2String(type),"|",Entry2String(entry),"|",volume,"|",price,"]");
//--- ejecución
   ProcessOrder(account,symbol,type,entry,volume,price);
   return(true);
  }

Ya que no todo el mundo dispone de 10 000$ en su cuenta, en la parte del cliente se realiza la recuenta del volumen comercial a través de la función GetLotSize(). La estrategia que trabaja en el lado del servidor puede utilizar su propio sistema de administración del capital. Por eso, lo mismo hay que hacer en la parte del cliente.

Yo propongo utilizar "Lot mapping" - el usuario puede especificar los márgenes de los posibles volúmenes de trading (valores mínimos y máximos), y el script en el lado del cliente hará la conversión automática:

extern string _1="--- LOT MAPPING ---";
extern double  InpMinLocalLotSize=0.01; // Márgenes de visualización del volumen
extern double  InpMaxLocalLotSize=1.00; // Se recomienda establecer el volumen mayor
extern double  InpMinRemoteLotSize =      0.01;
extern double  InpMaxRemoteLotSize =      15.00;
//+------------------------------------------------------------------+
//| Conversión del volumen de trading                                |
//+------------------------------------------------------------------+
double GetLotSize(string remote_lots,string symbol)
  {
   double dRemoteLots=StrToDouble(remote_lots);
   double dLocalLotDifference=InpMaxLocalLotSize-InpMinLocalLotSize;
   double dRemoteLotDifference=InpMaxRemoteLotSize-InpMinRemoteLotSize;
   double dLots=dLocalLotDifference *(dRemoteLots/dRemoteLotDifference);
   double dMinLotSize=MarketInfo(symbol,MODE_MINLOT);
   if(dLots<dMinLotSize)
      dLots=dMinLotSize;
   return(NormalizeDouble(dLots,InpVolumePrecision));
  }

En el lado del cliente se soportan los brokers con las cotizaciones de 4 y 5 dígitos, así como se soportan los volúmenes "regular-lot" (0.1) y "mini-lot" (0.01). Por eso he tenido que crear el nuevo tipo de la transacción DEAL_OUTALL.

Dado que la conversión del volumen de la transacción se realiza en la parte del cliente, pueden surgir situaciones cuando las transacciones con pequeños volúmenes quedan sin cerrar.

//+------------------------------------------------------------------+
//| Procesamiento de la orden                                        |
//| (los datos están convertidos y comprobados)                      |
//+------------------------------------------------------------------+
void ProcessOrder(int account,string symbol,int type,int entry,double volume,double price)
  {
   if(entry==OP_IN)
     {
      DealIN(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_OUT)
     {
      DealOUT(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_INOUT)
     {
      DealOUT_ALL(symbol,type,account);
      DealIN(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_OUTALL)
     {
      DealOUT_ALL(symbol,type,account);
     }
  }

5.3. Posiciones MetaTrader 5 vs Órdenes MetaTrader 4

Durante la implementación, me ha surgido otro problema. Es que en MetaTrader 5 siempre puede haber sólo una posición para cada símbolo, mientras que en MetaTrader 4 la situación es totalmente diferente. Para hacer que esta correspondencia sea más cercana posible, cada una de las transacciones con un símbolo en concreto en esta dirección se cubre con varias órdenes en la parte de MetaTrader 4.

Cada nueva transacción "IN" es una nueva orden, luego sigue la transacción "OUT", el funcional del cierre se implementa en 3 pasos:

  1. Repasar todas las órdenes abiertas y cerrar las que no corresponden al volumen de la transacción. Si no hay dichas órdenes, ejecutar el punto 2;
  2. Repasar todas las órdenes abiertas y cerrar las órdenes cuyo volumen es menor que el volumen especificado OUT. Si después de eso han quedado las órdenes no cerradas, ejecutar el punto 3;
  3. Cerrar la orden cuyo volumen es mayor que el volumen solicitado, y a continuación abrir una nueva orden con el volumen que debe quedarse. Normalmente no se debe llegar a este paso, está previsto por motivos de seguridad.
//+------------------------------------------------------------------+
//| Procesamiento DEAL ENTRY OUT                                     |
//+------------------------------------------------------------------+
void DealOUT(string symbol, int cmd, double volume, double price, double stoploss, double takeprofit, int account)
{
   int type = -1;
   int i=0;
   
   if(cmd==OP_SELL)
      type = OP_BUY;
   else if(cmd==OP_BUY)
      type = OP_SELL;  
   
   string comment = "OUT."+Type2String(cmd);
   //--- búsqueda de las órdenes con el volumen que es igual al establecido y con el beneficio > 0
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()==volume)
                  {
                     if(OrderProfit()>0)
                     {
                        if(CloseOneOrder(OrderTicket(), symbol, type, volume))
                        {
                           Print("Order with exact volume and profit>0 found and executed.");
                           return;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- búsqueda de las órdenes con el volumen que es igual al establecido, con cualquier beneficio 
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()==volume)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, volume))
                     {
                        Print("Order with exact volume found and executed.");
                        return;
                     }
                  }
               }
            }
         }
      }
   }
   double volume_to_clear = volume;
   //--- búsqueda de las órdenes con el volumen menor que el establecido y con el beneficio > 0
   int limit = OrdersTotal();
   for(i=0;i<limit;i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()<=volume_to_clear)
                  {
                     if(OrderProfit()>0)
                     {
                        if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                        {
                           Print("Order with smaller volume and profit>0 found and executed.");
                           volume_to_clear-=OrderLots();
                           if(volume_to_clear==0)
                           {
                              Print("All necessary volume is closed.");
                              return;
                           }
                           limit = OrdersTotal();
                           i = -1;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- búsqueda de las órdenes con el volumen menor que el establecido
   limit = OrdersTotal();
   for(i=0;i<limit;i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()<=volume_to_clear)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                     {
                        Print("Order with smaller volume found and executed.");
                        volume_to_clear-=OrderLots();
                        if(volume_to_clear==0)
                        {
                           Print("All necessary volume is closed.");
                           return;
                        }
                        limit = OrdersTotal();
                        //--- ya que el valor va a ser aumentado y al final del ciclo será igual a 0.
                        i = -1; 
                     }
                  }
               }
            }
         }
      }
   }
   //--- búsqueda de las órdenes con el volumen mayor que el establecido
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()>=volume_to_clear)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                     {
                        Print("Order with smaller volume found and executed.");
                        volume_to_clear-=OrderLots();
                        if(volume_to_clear<0)//Closed too much
                        {
                           //--- abrir nueva orden
                           DealIN(symbol,type,volume_to_clear,price,OrderStopLoss(),OrderTakeProfit(),account);
                        }
                        else if(volume_to_clear==0)
                        {
                           Print("All necessary volume is closed.");
                           return;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   if(volume_to_clear!=0)
   {
      Print("Some volume left unclosed: ",volume_to_clear);
   }
}

Conclusiones

Desde luego, se puede mejorar la solución propuesta aquí (por ejemplo, el protocolo de servidor, trabajo con las conexiones, ejecución de transacciones). Mi tarea consistía en comprobar las posibilidades de la idea e implementar el funcional básico que puede ser usado por cualquier persona que lo desee.

La solución propuesta funciona bastante bien y puede ser utilizada para copiar las señales de sus propias estrategias, o bien para copiar las transacciones de los participantes de Automated Trading Championship. El rendimiento y las posibilidades que ofrecen los lenguajes MQL4 y MQL5 son suficientes para el uso profesional y comercial. Yo creo que se puede crear muy buen proveedor de señales comerciales para todos los clientes de MetaTrader 4 y MetaTrader 5 utilizando sólo su ordenador y su propia estrategia.

Me gustaría ver las mejoras del código que he expuesto aquí, así como conocer sus opiniones y recibir sus comentarios. Si tienen algunas preguntas, intentaré contestar a ellas. En este momento he iniciado la prueba en la que se realiza el seguimiento de las transacciones de mis participantes preferidos del Campeonato. Por ahora, durante una semana, todo funciona bien. Si surge algún problema, los códigos serán actualizados.

Advertencia del autor (Tsaktuo):

Por favor, tenga en cuenta, si utiliza el funcional expuesto en presente artículo en una cuenta real, asumirá toda la responsabilidad por sufrir cualquier posible pérdida o perjuicio. Opere en una cuenta real SÓLO después de realizar una minuciosa prueba y SÓLO después de entender completamente las particularidades de implementación del funcional expuesto en este artículo.