El método óptimo para el cálculo del volumen total de una posición mediante un número mágico determinado

Dmitry Fedoseev | 11 marzo, 2014

Introducción

MetaTrader 5 permite el funcionamiento en paralelo de varios Asesores Expertos con un símbolo. Es sencillo; solo hay que abrir varios gráficos y añadirles los Asesores Expertos. Estaría bien si cada Asesor Experto pueda trabajar independientemente desde otro Asesor Experto que esté trabajando con el mismo símbolo (no existe dicho problema para los Asesores Expertos trabajando con símbolos distintos).

En primer lugar, permitirá a un Asesor Experto operar en toda coherencia con el rendimiento y la optimización de sus pruebas en el Probador de Estrategias. Las condiciones para la apertura de una posición pueden depender del tamaño o de la ausencia de posiciones ya abiertas. Si varios Asesores Expertos trabajan con el mismo símbolo, se afectan mutuamente.

La segunda cosa y probablemente la más importante, es permitir a los Asesores Expertos utilizar sistemas de gestión de fondos distintos, según las estrategias de trading implementadas en los Asesores Expertos. Y finalmente, la posibilidad de hacer un seguimiento de los resultados de cada Asesor Experto y detenerlo si es necesario.

1. Los principios generales del cálculo del volumen de una posición

Cuando abre una posición, puede marcarla con un número mágico indicando el valor de la variable mágica en la estructura MqlTradeRequest, que recibe la función OrderSend(). Al ejecutar la orden, la transacción también se marca con el número mágico de esta orden. Además, analizando las transacciones en el historial, podemos ver las transacciones abiertas por distintos Asesores Expertos.

El método de cálculo del volumen total de una posición es bastante sencillo: por ejemplo, si ejecuta una transacción de compra con un volumen de 0.1, luego una compra de 0.1 y una venta de 0.1, el volumen total de la posición será igual a 0.1+0.1-0.1=+0.1 Sumamos los volúmenes de las transacciones de compra y restamos los volúmenes de las transacciones de venta y obtenemos el volumen total de la posición.

Es importante comenzar los cálculos cuando el volumen total de la posición es igual a 0. El primer punto y el más evidente es el momento de apertura de la cuenta. En otras palabras, puede pedir el historial de todas las transacciones de la cuenta mediante la función HistorySelect() con el primer parámetro igual a 0 (el menor tiempo posible) y el valor del segundo parámetro TimeCurrent() (el tiempo conocido más reciente del servidor):

HistorySelect(0,TimeCurrent()); // load all history

A continuación, recorre todo el historial de principio a fin, sumando los volúmenes de las transacciones de compra y restando los volúmenes de las transacciones de venta para cada transacción con el número mágico especificado. También es una solución, pero en la práctica, el historial de las transacciones puede ser bastante voluminoso. Esto puede afectar significativamente a la velocidad el Asesor Experto, especialmente durante las pruebas y la optimización, hasta el punto de imposibilitar la utilización práctica de un Asesor Experto. Tenemos que encontrar el último momento del historial de las transacciones, cuando el volumen neto total de las posiciones era igual a cero.

Para hacerlo, primero tenemos que recorrer todo el historial y encontrar el último punto, cuando el volumen neto total de la posición era cero. Al encontrar este punto, lo guardamos en una determinada variable (tiempo de la posición fijada). Más adelante, el Asesor Experto recorrerá el historial de las transacciones a partir del punto que hemos guardado. La mejor solución es guardar este punto en una variable global del terminal de cliente en lugar de una variable del Asesor Experto, ya que en este caso se destruirá al desvincular el Asesor Experto.

En tal caso, hasta durante la ejecución del Asesor Experto, necesita cargar el historial estrictamente necesario, en lugar del historial entero de las transacciones. Hay muchos Asesores Expertos que pueden operar con el mismo símbolo, así que compartiremos esta variable global (con el tiempo guardado del último punto que corresponde al volumen cero) con todos los Asesores Expertos.

Vamos a desviarnos del tema principal y tratar el uso de las variables globales del terminal de cliente, que permite a varios Asesores Expertos trabajar con el mismo símbolo (tal vez con parámetros distintos), y evita la coincidencia de los nombres, creados por distintas instancias de los Asesores Expertos.

2. Utilización de las variables globales del terminal de cliente

El lenguaje MQL5 tiene la función MQLInfoString(), que permite obtener distintas informaciones acerca de un programa mql5.

Para obtener información acerca del nombre del archivo, llame a esta función con el identificador MQL_PROGRAM_NAME:

MQL5InfoString(MQL_PROGRAM_NAME); // Expert Advisor name

Por lo tanto, los nombres de las variables globales empiezan con el nombre de un Asesor Experto. Un Asesor Experto puede trabajar con varios símbolos; esto quiere decir que tenemos que añadir el nombre de un símbolo (Symbol). Los Asesores Expertos pueden trabajar con el mismo símbolo, pero con distintos períodos de tiempo (con distintas configuraciones), tenemos que utilizar el número mágico para estos casos. Por lo tanto, tenemos que añadir el número mágico.

Por ejemplo, si el Asesor Experto tiene un número mágico, almacenado en la variable Magic_N, lo añadimos al nombre de la variable global.

Los nombres de todas las variables globales tendrán el siguiente formato:

gvp=MQL5InfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // name of an Expert Advisor and symbol name 
                                                                            // and its magic number

donde gvp (prefijo de la variable global), es una variable tipo cadena, declarada en la sección de variables comunes.

Me gustaría aclarar la terminología para evitar la confusión de las variables globales, ya que se utilizan en la programación (las variables globales son visibles dentro de todas las funciones, las variables locales de una función solo son visibles dentro de dicha función).

Pero aquí tenemos un caso distinto, el término "variables globales" se refiere a las variables globales del terminal de cliente (variables especiales, guardadas en un archivo, disponibles mediante las funciones GlobalVariable...()). Cuando hablamos de variables globales (tal y como se utilizan en programación), usaremos el términos "variables comunes". El término variables locales se refiere a la variables locales.

Las variables globales son muy útiles, porque guardan sus valores después de la desinicialización de un Asesor Experto (reinicio del Asesor Experto, terminal de cliente, ordenador), pero en el modo de prueba hace falta borrar todas las variables (o un envío previo durante la optimización). La variables globales utilizadas en las operaciones reales deben estar separadas de las variables globales creadas durante la prueba, es necesario borrarlas después de la prueba. Pero no se deben modificar o borrar las variables globales creadas por el Asesor Experto.

Mediante la función AccountInfoInteger() y llamándola con el identificador ACCOUNT_TRADE_MODE, puede distinguir el modo actual: probador, de prueba, o cuenta real.

Vamos a añadir un prefijo a las variables globales: "d" -al trabajar con cuentas de prueba, "r" -al trabajar con cuentas reales, "t" -al trabajar con el Probador de Estrategias:

gvp=MQL5InfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // name of an Expert Advisor, symbol name
                                                                            // and the Expert Advisor magic number
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO))
  {
   gvp=gvp+"d_"; // demo account
  }
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL)
  {
   gvp=gvp+"r_"; // real
  }
if(MQL5InfoInteger(MQL_TESTER))
  {
   gvp=gvp+"t_"; // testing
  } 

Se debe llamar a la función desde la función OnInit() del Asesor Experto.

Como se mencionó antes, hay que borrar las variables globales al hacer la prueba, en otras palabras, tenemos que añadir la función que borra las variables globales en la función OnDeinit() del Asesor Experto:

void fDeleteGV()
  {
   if(MQL5InfoInteger(MQL_TESTER)) // Testing mode
     {
      for(int i=GlobalVariablesTotal()-1;i>=0;i--) // Check all global variables (from the end, not from the begin)
        {
         if(StringFind(GlobalVariableName(i),gvp,0)==0) // search for the specified prefix
           {
            GlobalVariableDel(GlobalVariableName(i)); // Delete variable
           }
        }
     }
  }

En la actualidad es imposible interrumpir las pruebas en MetaTrader 5, es decir, que la ejecución de la función OnDeinit() no está garantizada, no obstante, puede ocurrir en el futuro. No sabemos si la función OnDeinit() se va a ejecutar después de la interrupción del Probador de Estrategias, por lo tanto, borramos las variables globales al principio de la ejecución del Asesor Experto en la función OnInit().

Obtendremos el siguiente código de las funciones OnInit() y OnDeinit():

int OnInit()
  {
   fCreateGVP(); // Creating a prefix for the names of global variables of the client terminal
   fDeleteGV();  // Delete global variables when working in Tester
   return(0);
  }

void OnDeinit(const int reason)
  {
   fDeleteGV();  // Delete global variables when working in tester
  }

También podemos simplificar el uso de las variables globales, creando funciones con nombres cortos para la creación de las variables globales (en lugar de GlobalVariableSet(gvp+...),.

La función para establecer el valor de la variable global:

void fGVS(string aName,double aValue)
  {
   GlobalVariableSet(gvp+aName,aValue);
  }

La función para obtener el valor de la variable global:

double fGVG(string aName)
  {
   return(GlobalVariableGet(gvp+aName));
  }

La función para eliminar la variable global:

void fGVD(string aName)
  {
   GlobalVariableDel(gvp+aName);
  }

Hemos analizado las variables globales, pero eso no es todo.

Tenemos que ofrecer la posibilidad de crear variables globales para un símbolo, y proporcionar sus distintas operaciones en la cuenta y en el Probador de Estrategias. Los nombres de estas variables globales no tiene que depender del nombre o del número mágico de un Asesor Experto.

Vamos a declarar otra variable para el prefijo de la variable global, que nombramos "Commom_gvp". Al utilizarla con la cuenta, tendrá el valor "COMMON", y tendrá el mismo valor que la variable gvp al funcionar con el Probador de Estrategias (para borrar la variable antes o después del procedimiento de la prueba retroactiva de la estrategia).

Finalmente, la función que prepara los prefijos de las variables globales tiene el siguiente formato:

void fCreateGVP()
  {
   gvp=MQL5InfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_";
   Commom_gvp="COMMOM_"; // Prefix for common variables for all Expert Advisors
   if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO)
     {
      gvp=gvp+"d_";
     }
   if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL)
     {
      gvp=gvp+"r_";
     }
   if(MQL5InfoInteger(MQL_TESTER))
     {
      gvp=gvp+"t_";
      Commom_gvp=gvp; // To be used in tester, the variables with such a prefix 
                      // will be deleted after the testing
     }
  }

Alguien puede pensar que los prefijos de las variables globales incluyen información adicional, la separación de las cuentas reales y de prueba, y el prefijo "t" para las pruebas, aunque podría hacerse simplemente añadiendo la letra "t", que indica que nuestro Asesor Experto está trabajando en modo de Probador e Estrategias. Pero lo hice de esta manera. No conocemos el futuro ni las cosas que puedan hacer falta para analizar el trabajo de los Asesores Expertos.

Como dicen, lo que abunda no daña.

Las funciones mencionadas antes implican que el terminal de cliente funciona con una cuenta, no hay ningún cambio de cuenta durante su funcionamiento. Está prohibido cambiar de cuenta durante el funcionamiento de un Asesor Experto. Por supuesto, si es necesario, se puede resolver este problema añadiendo un número de cuenta a los nombres de las variables globales.

¡Otra observación muy importante! La longitud del nombre de la variable global está limitada a 63 caracteres. Por consiguiente, no ponga nombres largos a sus Asesores Expertos.

Hemos terminado con las variables globales, ahora es el momento de volver al tema principal del artículo -el cálculo del volumen de una posición mediante un número mágico determinado.

3. El cálculo del volumen de una posición:

En primer lugar, vamos a comprobar si hay una variable global con la información acerca del último momento de la posición de volumen cero, mediante la función GlobalVariableCheck() (para simplificar, si no hay ninguna posición abierta, lo llamaremos el caso "posición cero").

Si existe esta variable, vamos a cargar el historial de las transacciones empezando desde el momento almacenado en la variable, de lo contrario cargaremos todo el historial:

if(GlobalVariableCheck(Commom_gvp+sSymbol+"_HistStTm")) // Saved time of a "zero" total position
  {
   pLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm"); // initial date setting 
                                                                             // select only the history needed
  }
else
 {
   GlobalVariableSet(Commom_gvp+sSymbol+"_HistStTm",0);
 }
if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // Load the necessary part of the deal history
  { 
   return(false);
  } 

A continuación, definimos el volumen neto total de la posición para un símbolo:

double CurrentVolume=fSymbolLots(pSymbol);

Se determina el volumen de una posición mediante la función fSymbolLots().

Hay varias maneras de obtener el volumen de una posición: por ejemplo, se puede hacer mediante la función PositionSelect(). Si la función devuelve false, significa que no hay ninguna posición (su volumen es igual a cero). Si la función devuelve true, entonces se puede obtener el volumen mediante la función PositionGetDouble() con el identificador POSITION_VOLUME. Se determina el tipo de posición (compra o venta) mediante la función PositionGetInteger() con el identificador POSITION_TYPE. La función devuelve un valor positivo para las posiciones largas y negativo para las cortas.

La función completa se muestra a continuación:

double fSymbolLots(string aSymbol)
  {
   if(PositionSelect(aSymbol,1000)) // the position has been selected successfully, so it exists
     {
      switch(PositionGetInteger(POSITION_TYPE)) // It returns the positive or negative value dependent on the direction
        {
         case POSITION_TYPE_BUY:
            return(NormalizeDouble(PositionGetDouble(POSITION_VOLUME),2));
            break;
         case POSITION_TYPE_SELL:
            return(NormalizeDouble(-PositionGetDouble(POSITION_VOLUME),2));
            break;
        }
     }
   else
     {
      return(0);
     }
  }

Por otro lado, puede determinar el volumen total de la posición de un símbolo mediante un bucle que recorre todas las posiciones, se determina el número de posiciones mediante la función PositionsTotal(). Luego, encuentra el símbolo necesario mediante la función PositionGetSymbol(), y determina el volumen y la dirección de la posición (la función PositionGetDouble() con el identificador POSITION_VOLUME y la función PositionGetInteger() con el identificador POSITION_TYPE).

En este caso, esta será la función: 

double fSymbolLots(string aSymbol)
  {
   double TmpLots=0;
   for(int i=0;i<PositionsTotal();i++) // Go through all positions
     {
      if(PositionGetSymbol(i)==aSymbol) // we have found a position with specified symbol
        {
         TmpLots=PositionGetDouble(POSITION_VOLUME);
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            TmpLots*=-1; // the sign is dependent on the position type           }
         break;
        }
     }
   TmpLots=NormalizeDouble(TmpLots,2);
   return(TmpLots);
  }

Después de determinar el volumen actual, vamos a recorrer el historial de las transacciones de principio a fin, hasta que la suma de los volúmenes sea igual al volumen actual.

Se determina la longitud del historial elegido de transacciones mediante la función HistoryDealsTotal(), se determina el ticket para cada transacción mediante la función HistoryDealGetTicket(), se extraen los datos de la transacción mediante la función HistoryDealGetInteger() (el identificador DEAL_TYPE para el tipo de transacción) y HistoryDealGetDouble() (el identificador DEAL_VOLUME para el volumen de la transacción):

double Sum=0; 
int FromI=0;
int FromTicket=0;
for(int i=HistoryDealsTotal()-1;i>=0;i--) // go through all the deals from the end to the beginning 
  {
   ulong ticket=HistoryDealGetTicket(i); // Get ticket of the deal
   if(ticket!=0)
     {
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // We add or subtract the volume depending on deal direction
        {
         case DEAL_TYPE_BUY:
            Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            Sum=NormalizeDouble(Sum,2);
            break;
         case DEAL_TYPE_SELL:
            Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            Sum=NormalizeDouble(Sum,2);
            break;
        }
      if(CurrentVolume==Sum) // all the deals has scanned
        {
         sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME); // Save the time of a "zero" position
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
         FromI=i; // Save the index
         break;
        }
     }
  }

Cuando encontramos este punto, almacenamos el tiempo en la variable global, que se va a utilizar más adelante al cargar el historial de las transacciones (el índice de la transacción en el historial se almacena en la variable FromI).

Antes de la transacción con índice FromI, el volumen total de la posición en el símbolo era igual a cero.

Ahora recorremos el historial desde FromI hasta el final y contamos el volumen de las transacciones con el número mágico especificado.

static double sVolume=0;
static ulong sLastTicket=0;
for(int i=FromI;i<HistoryDealsTotal();i++) // from the first deal until the end
  {
   ulong ticket=HistoryDealGetTicket(i);   // Get deal ticket
   if(ticket!=0)
     {
      if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol) // Specified symbol
        {
         long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
         if(PosMagic==aMagic || aMagic==-1) // Specified magic
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // add or subtract the deal volumes 
                                                       // depending on the deal type
              {
               case DEAL_TYPE_BUY:
                  sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  sLastTicket=ticket;
                  break;
               case DEAL_TYPE_SELL:
                  sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  sLastTicket=ticket;
                  break;
              }
           }
        }
     }
  } 

Al final del bucle tendremos el volumen de la posición actual mediante el número mágico especificado, se almacena el ticket de la última transacción con el número mágico especificado en la variable sLastTicket, después de la ejecución de la transacción, el volumen total de una posición con el número mágico especificado será igual a sVolume. Ha finalizado el trabajo preliminar de la función.

Se declaran las variables sLoadHistoryFrom, sLastTicket y sVolume como estáticas (almacenan sus valores después de que la función haya terminado), más adelante, se utilizarían estos valores para cada llamada de la función.

Tenemos el tiempo (el punto de partida del historial de las transacciones), el ticket de la transacción, y después de su ejecución, el volumen total de la posición (con el símbolo indicado) tendrá el valor actual.

Gracias al tiempo de la posición de volumen cero, basta con recorrer el historial desde el tiempo actual hasta el tiempo almacenado y realizar la suma de los volúmenes de las transacciones y guardar el volumen y el ticket de la última transacción.

Por lo tanto, el cálculo del volumen total de la posición del Asesor Experto consiste en el procesamiento de las últimas transacciones:

if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // Request for the deals history up to the current time
  {
   return(false);
  }
for(int i=HistoryDealsTotal()-1;i>=0;i--) // Loop from the end
  {
   ulong ticket=HistoryDealGetTicket(i); // Get ticke
   if(ticket!=0)
     {
      if(ticket==sLastTicket) // We have found the already calculated deal, save the ticket and break
        {
         sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
         break;
        }
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // Add or subtract deal volume depending on deal type      
        {
         case DEAL_TYPE_BUY:
            sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
         case DEAL_TYPE_SELL:
            sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
        }
     }
  }

A continuación se muestra el algoritmo de la función:


La función completa:

bool fGetPositionVolume(string aSymbol,int aMagic,double aVolume)
  {
   static bool FirstStart=false;
   static double sVolume=0;
   static ulong sLastTicket=0;
   static datetime sLoadHistoryFrom=0;
   // First execution of function when Expert Advisor has started
   if(!FirstStart)
     {
      if(GlobalVariableCheck(Commom_gvp+aSymbol+"_HistStTm"))
        {
         sLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+aSymbol+"_HistStTm");
        }
      else
        {
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",0);
        }
      if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // Return if unsuccessful, 
                                                      // we will repeat on the next tick
        {
         return(false);
        }
      double CurrentVolume=fSymbolLots(aSymbol); // Total volume
      double Sum=0;
      int FromI=0;
      int FromTicket=0;
      // Search the last time when position volume was equal to zero
      for(int i=HistoryDealsTotal()-1;i>=0;i--)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
              {
               case DEAL_TYPE_BUY:
                  Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
               case DEAL_TYPE_SELL:
                  Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
              }
            if(CurrentVolume==Sum)
              {
               sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME);
               GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
               FromI=i;
               break;
              }
           }
        }
      // Calculate the volume of position with specified magic number and symbol
      for(int i=FromI;i<HistoryDealsTotal();i++)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol)
              {
               long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
               if(PosMagic==aMagic || aMagic==-1)
                 {
                  switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                    {
                     case DEAL_TYPE_BUY:
                        sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        sLastTicket=ticket;
                        break;
                     case DEAL_TYPE_SELL:
                        sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        sLastTicket=ticket;
                        break;
                    }
                 }
              }
           }
        }
      FirstStart=true;
     }

   // Recalculate the volume of a position (with specified symbol and magic)
   // for the deals, after the zero position time
   if(!HistorySelect(sLoadHistoryFrom,TimeCurrent()))
     {
      return(false);
     }
   for(int i=HistoryDealsTotal()-1;i>=0;i--)
     {
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket!=0)
        {
         if(ticket==sLastTicket)
           {
            sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
            break;
           }
         switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
           {
            case DEAL_TYPE_BUY:
               sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
               break;
            case DEAL_TYPE_SELL:
               sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
               break;
           }
        }
     }
   aVolume=NormalizeDouble(sVolume,2);;
   return(true);
  }

Se envía el símbolo y el número mágico a la función que a su vez devuelve el volumen de la posición. En caso de éxito, devuelve true, de lo contrario, devuelve false.

Si hay éxito, devuelve el volumen requerido a la variable aVolume, que se envía a la función por referencia. Las variables estáticas declaradas en la función, no permiten el uso de esta función con distintos parámetros (símbolo y número mágico).

En el caso de MQL4, se resuelve este problema mediante la creación de una copia de esta función con un nombre diferente y llamarla para el otro par "símbolo-número mágico" o declarar las variables FirstStart, sVolume, sLastTicket y sLoadHistoryFrom como variables comunes -para cada par "símbolo-número mágico" y enviarlos a la función.

También se puede implementar de la misma manera en MQL5, pero MQL5 tiene una función mucho más cómoda -las clases, es el caso en el que el uso de las clases es idóneo. Al utilizar las clases, es necesario crear una instancia de clase para cada par "símbolo-número mágico", Se almacenarán los datos en cada instancia de clase.

Vamos a declarar la clase PositionVolume. Todas las variables declaradas como estáticas en la función serán declaradas como privadas (private), no las vamos a utilizar directamente desde el Asesor Experto, excepto la variable Volume. Pero solo nos hará falta después de la ejecución de la función de cálculo del volumen. También declaramos las variables Symbol y Magic -es muy poco práctico enviarlas a la función, solamente se hace una vez al inicializar la instancia de clase.

La clase tendrá dos funciones públicas (public): la función de inicialización y la función del cálculo del volumen de la posición, y una función privada (private) para determinar el volumen total de la posición:

class PositionVolume
  {
private:
   string            pSymbol;
   int               pMagic;
   bool              pFirstStart;
   ulong             pLastTicket;
   double            pVolume;
   datetime         pLoadHistoryFrom;
   double            SymbolLots();
public:
   void Init(string aSymbol,int aMagic)
     {
      pSymbol=aSymbol;
      pMagic=aMagic;
      pFirstStart=false;
      pLastTicket=0;
      pVolume=0;
     }
   bool              GetVolume(double  &aVolume);
  }; 
bool PositionVolume::GetVolume(double  &aVolume)
  {
   if(!pFirstStart)
     {
      if(GlobalVariableCheck(Commom_gvp+pSymbol+"_HistStTm"))
        {
         pLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm");
        }
      else
        {
         GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",0);
        }
      if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
        {
         return(false);
        }
      double CurrentVolume=fSymbolLots(pSymbol);
      double Sum=0;
      int FromI=0;
      int FromTicket=0;
      for(int i=HistoryDealsTotal()-1;i>=0;i--)
        {

         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
              {
               case DEAL_TYPE_BUY:
                  Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
               case DEAL_TYPE_SELL:
                  Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
              }
            if(CurrentVolume==Sum)
              {
               pLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME);
               GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",pLoadHistoryFrom);
               FromI=i;
               break;
              }
           }
        }
      for(int i=FromI;i<HistoryDealsTotal();i++)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol)
              {
               long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
               if(PosMagic==pMagic || pMagic==-1)
                 {
                  switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                    {
                     case DEAL_TYPE_BUY:
                        pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        pLastTicket=ticket;
                        break;
                     case DEAL_TYPE_SELL:
                        pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        pLastTicket=ticket;
                        break;
                    }
                 }
              }
           }
        }
      pFirstStart=true;
     }
   if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
     {
      return(false);
     }
   for(int i=HistoryDealsTotal()-1;i>=0;i--)
     {
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket!=0)
        {
         if(ticket==pLastTicket)
           {
            break;
           }
         if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol)
           {
            long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
            if(PosMagic==pMagic || pMagic==-1)
              {
               switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                 {
                  case DEAL_TYPE_BUY:
                     pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                     break;
                  case DEAL_TYPE_SELL:
                     pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                     break;
                 }
              }
           }
        }
     }
   if(HistoryDealsTotal()>0)
     {
      pLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
     }
   pVolume=NormalizeDouble(pVolume,2);
   aVolume=pVolume;
   return(true);
  }

double PositionVolume::SymbolLots()
  {
   double TmpLots=0;
   for(int i=0;i<PositionsTotal();i++)
     {
      if(PositionGetSymbol(i)==pSymbol)
        {
         TmpLots=PositionGetDouble(POSITION_VOLUME);
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            TmpLots*=-1;
           }
         break;
        }
     }
   TmpLots=NormalizeDouble(TmpLots,2);
   return(TmpLots);
  }

Al utilizar esta clase para cada par "símbolo-número mágico", es necesario crear una instancia de clase.

PositionVolume PosVol11;
PositionVolume PosVol12;
PositionVolume PosVol21;
PositionVolume PosVol22;

Hay que inicializarla en la función OnInit() de un Asesor Experto, por ejemplo:

PosVol11.Init(Symbol_1,Magic_1); 
PosVol12.Init(Symbol_1,Magic_2);
PosVol21.Init(Symbol_2,Magic_1); 
PosVol22.Init(Symbol_2,Magic_2);   

Después de esto, es posible conseguir el volumen de una posición mediante el símbolo y el número mágico especificado. Llamemos a la función GetVolume de la correspondiente instancia de clase.

En caso de éxito, devuelve true y almacena el valor en la variable, que se envía por referencia como parámetro a la función:

double Vol11;
double Vol12;
double Vol21;
double Vol22;
PosVol11.GetVolume(Vol11);
PosVol12.GetVolume(Vol12);
PosVol21.GetVolume(Vol21);
PosVol22.GetVolume(Vol22);

Se puede decir aquí que ya está todo hecho, pero falta la prueba de control.

4. Prueba de control

Para probar el funcionamiento de la función, hemos utilizado un Asesor Experto que funciona simultáneamente con 4 posiciones:

  1. mediante el indicador RSI con un período 14 sobre EURUSD con número mágico 1;
  2. mediante el indicador RSI con un período 21 sobre EURUSD con número mágico 2;
  3. mediante el indicador RSI con un período 14 sobre GBPUSD con número mágico 1;
  4. mediante el indicador RSI con un período 21 sobre GBPUSD con número mágico 2;

El Asesor Experto con el número mágico 1 realizó una operación con un volumen de lote de 0.1 y el Asesor Experto con el número mágico 2 realizó una operación con un volumen de lote de 0.2.

Se suma el volumen de una transacción a las variables del Asesor Experto al ejecutar la transacción, antes y después de la transacción se ha determinado el volumen de cada posición mediante la función mencionada anteriormente. 

La función genera un mensaje si surge un error en el cálculo de los volúmenes.

Se puede encontrar el código del Asesor Experto en el archivo adjunto al artículo (nombre del archivo: ePosVolTest.mq5).

Conclusión

Un Asesor Experto requiere muchas funciones, y hay que implementarlas de manera que su uso sea cómodo en todas las etapas. Hay que escribir estas funciones respetando las reglas del mejor uso de los recursos de computación.

El método de cálculo del volumen de la posición propuesto en este artículo, satisface estas condiciones; carga solamente la parte estrictamente mínima del historial de las transacciones al ponerlo en marcha. Durante su trabajo, recalcula el volumen actual de la posición mediante las últimas transacciones.