Libro de Recetas MQL5: Analizar Propiedades de Posición en el Probador de Estrategias de MetaTrader 5

Anatoli Kazharski | 28 mayo, 2014

Introducción

En este artículo modificaremos el Asesor Experto creado en el artículo anterior "MQL5 Cookbook: Position Properties on the Custom Info Panel" (“Propiedades de Posición en el Panel de Información Personalizada”) y trataremos los siguientes asuntos:

De hecho, cada uno de los asuntos mencionados arriba merece un artículo propio, pero en mi opinión este enfoque solo complicaría el estudio del lenguaje.

Usaré ejemplos muy sencillos para mostrarle cómo se pueden implementar estas prestaciones. En otras palabras, la implementación de cada una de las tareas mencionadas arriba se pondrá literalmente en una función simple y directa. Al desarrollar una determinada idea en los siguientes artículos de la serie, poco a poco haremos estas funciones más complejas, según sea necesario y hasta el punto que lo requiera la tarea correspondiente.

Primero, copiemos el Asesor Experto del artículo anterior, puesto que necesitaremos todas sus funciones.


Desarrollar un Asesor Experto

Comenzaremos incluyendo la clase CTrade de la Biblioteca Estándar en su archivo. Esta clase tiene todas las funciones necesarias para ejecutar operaciones de trading. Para empezar, podemos usarlas fácilmente, sin ni siquiera mirar dentro, que es precisamente lo que vamos a hacer.

Para incluir la clase deberemos escribir lo siguiente:

//--- Include a class of the Standard Library
#include <Trade/Trade.mqh>

Puede colocar este código al principio del archivo para poder encontrarlo fácilmente después, por ejemplo después de la directiva #define. El comando #include denota que el archivo Trade.mqh debe tomarse de <MetaTrader 5 terminal directory>\MQL5\Include\Trade\. Se puede usar el mismo método para incluir cualquier otro que contenga funciones. Esto es especialmente útil cuando la cantidad de código del proyecto se hace más grande y es más difícil manejarlo.

Ahora debemos crear una instancia de la clase para tener acceso a todas sus funciones. Esto se puede hacer escribiendo el nombre de la instancia después del nombre de la clase:

//--- Load the class
CTrade trade;

En esta versión del Asesor Experto usaremos solo una función de trading de todas las funciones disponibles en la clase CTrade. Es la función PositionOpen(), que se usa para abrir una posición. También se puede usar para la inversión de una posición ya abierta. Más tarde, en este artículo, mostraremos cómo se puede llamar a esta función desde la clase al crear una función responsable para la ejecución de operaciones de trading.

Además, añadiremos dos arrays dinámicos a nivel global. Estos arrays tomarán valores de barra.

//--- Price data arrays
double               close_price[]; // Close (closing prices of the bar)
double               open_price[];  // Open (opening prices of the bar)

A continuación, crearemos una función CheckNewBar() que, al usarla, el programa comprobará si hay eventos de barra nueva, puesto que las operaciones de trading solo se ejecutarán en barras completadas.

Debajo está el código de la función CheckNewBar() con comentarios detallados:

//+------------------------------------------------------------------+
//| CHECKING FOR THE NEW BAR                                         |
//+------------------------------------------------------------------+
bool CheckNewBar()
  {
//--- Variable for storing the opening time of the current bar
   static datetime new_bar=NULL;
//--- Array for getting the opening time of the current bar
   static datetime time_last_bar[1]={0};
//--- Get the opening time of the current bar
//    If an error occurred when getting the time, print the relevant message
   if(CopyTime(_Symbol,Period(),0,1,time_last_bar)==-1)
     { Print(__FUNCTION__,": Error copying the opening time of the bar: "+IntegerToString(GetLastError())+""); }
//--- If this is a first function call
   if(new_bar==NULL)
     {
      // Set the time
      new_bar=time_last_bar[0];
      Print(__FUNCTION__,": Initialization ["+_Symbol+"][TF: "+TimeframeToString(Period())+"]["
            +TimeToString(time_last_bar[0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]");
      return(false); // Return false and exit 
     }
//--- If the time is different
   if(new_bar!=time_last_bar[0])
     {
      new_bar=time_last_bar[0]; // Set the time and exit 
      return(true); // Store the time and return true
     }
//--- If we have reached this line, then the bar is not new, return false
   return(false);
  }

Como puede ver en el código de arriba, la función CheckNewBar() devuelve true si la barra es nueva, o false si todavía no hay barra nueva. De esta forma puede controlar la situación durante el trading o la simulación, ejecutando solo operaciones de trading en barras completadas.

Al comienzo de la función, declararemos una variable static y un array estático de tipo datetime. Las variables locales estáticas mantienen sus valores incluso después de que hayamos salido de la función. En cada llamada subsiguiente de la función, estas variables locales contendrán los valores que tomaron en la llamada anterior de la función.

Además, note la función CopyTime(): nos ayuda a obtener la fecha de la última barra en el array time_last_bar. Asegúrese de comprobar la sintaxis de la función en el material de referencia de MQL5.

También puede notar la función definida por el usuario TimeframeToString(), que hasta ahora no se había mencionado en esta serie de artículos. Convierte valores de intervalo cronológico a una cadena de caracteres más clara para el usuario:

string TimeframeToString(ENUM_TIMEFRAMES timeframe)
  {
   string str="";
   //--- If the passed value is incorrect, take the time frame of the current chart
   if(timeframe==WRONG_VALUE || timeframe == NULL)
      timeframe = Period();
   switch(timeframe)
     {
      case PERIOD_M1  : str="M1";  break;
      case PERIOD_M2  : str="M2";  break;
      case PERIOD_M3  : str="M3";  break;
      case PERIOD_M4  : str="M4";  break;
      case PERIOD_M5  : str="M5";  break;
      case PERIOD_M6  : str="M6";  break;
      case PERIOD_M10 : str="M10"; break;
      case PERIOD_M12 : str="M12"; break;
      case PERIOD_M15 : str="M15"; break;
      case PERIOD_M20 : str="M20"; break;
      case PERIOD_M30 : str="M30"; break;
      case PERIOD_H1  : str="H1";  break;
      case PERIOD_H2  : str="H2";  break;
      case PERIOD_H3  : str="H3";  break;
      case PERIOD_H4  : str="H4";  break;
      case PERIOD_H6  : str="H6";  break;
      case PERIOD_H8  : str="H8";  break;
      case PERIOD_H12 : str="H12"; break;
      case PERIOD_D1  : str="D1";  break;
      case PERIOD_W1  : str="W1";  break;
      case PERIOD_MN1 : str="MN1"; break;
     }
//---
   return(str);
  }

Más adelante en el artículo mostraremos cómo se usa la función CheckNewBar(), cuando ya tengamos todas las otras funciones necesarias listas. Ahora observemos la función GetBarsData(), que toma valores del número de barras solicitado.

//+------------------------------------------------------------------+
//| GETTING BAR VALUES                                               |
//+------------------------------------------------------------------+
void GetBarsData()
  {
//--- Number of bars for getting their data in an array
   int amount=2;
//--- Reverse the time series ... 3 2 1 0
   ArraySetAsSeries(close_price,true);
   ArraySetAsSeries(open_price,true);
//--- Get the closing price of the bar
//    If the number of the obtained values is less than requested, print the relevant message
   if(CopyClose(_Symbol,Period(),0,amount,close_price)<amount)
     {
      Print("Failed to copy the values ("
            +_Symbol+", "+TimeframeToString(Period())+") to the Close price array! "
            "Error "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- Get the opening price of the bar
//    If the number of the obtained values is less than requested, print the relevant message
   if(CopyOpen(_Symbol,Period(),0,amount,open_price)<amount)
     {
      Print("Failed to copy the values ("
            +_Symbol+", "+TimeframeToString(Period())+") to the Open price array! "
            "Error "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
  }

Observemos más en detalle el código de arriba. En primer lugar, en la variable amount especificamos el número de barras cuyos datos necesitamos obtener. A continuación configuramos el orden de indexación del array para que el valor de la última barra (actual) se encuentre en el índice cero del array, usando la función ArraySetAsSeries(). Por ejemplo, si desea usar el valor de la última barra en sus cálculos, se puede escribir tal y como se muestra a continuación, si se ejemplifica por el precio de apertura: open_price[0]. Similarmente, la anotación para la antepenúltima barra será: open_price[1].

El mecanismo para obtener precios de apertura y de cierre es similar al de la función CheckNewBar(), con la que obtuvimos la fecha de la última barra. Es justo que en este caso usemos las funciones CopyClose() y CopyOpen(). Similarmente, las funciones CopyHigh() y CopyLow() se usan para obtener los precios de barra alta y baja, respectivamente.

Veamos ahora un ejemplo muy sencillo que muestra cómo determinar señales para la apertura/inversión de una posición. Los arrays de precio almacenan datos para dos barras (la barra y la completada anteriormente). Usaremos datos de la barra completada.

El código para la implementación de estas simples condiciones se da abajo:

//+------------------------------------------------------------------+
//| DETERMINING TRADING SIGNALS                                      |
//+------------------------------------------------------------------+
int GetTradingSignal()
  {
//--- A Buy signal (0) :
   if(close_price[1]>open_price[1])
      return(0);
//--- A Sell signal (1) :
   if(close_price[1]<open_price[1])
      return(1);
//--- No signal (3):
   return(3);
  }

Como puede ver, es muy sencillo. Se puede determinar fácilmente cómo gestionar condiciones más complejas de forma similar. La función devuelve cero si una barra completada está arriba, o uno si una barra completada está abajo. Si por cualquier motivo no hay señal, la función devolverá 3.

Ahora solo debemos crear una función TradingBlock() para la implementación de actividades de trading. Abajo está el código de la función con comentarios detallados:

//+------------------------------------------------------------------+
//| TRADING BLOCK                                                    |
//+------------------------------------------------------------------+
void TradingBlock()
  {
   int               signal=-1;           // Variable for getting a signal
   string            comment="hello :)";  // Position comment
   double            start_lot=0.1;       // Initial volume of a position
   double            lot=0.0;             // Volume for position calculation in case of reverse position
   double            ask=0.0;             // Ask price
   double            bid=0.0;             // Bid price
//--- Get a signal
   signal=GetTradingSignal();
//--- Find out if there is a position
   pos_open=PositionSelect(_Symbol);
//--- If it is a Buy signal
   if(signal==0)
     {
      //--- Get the Ask price
      ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
      //--- If there is no position
      if(!pos_open)
        {
         //--- Open a position. If the position failed to open, print the relevant message
         if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,start_lot,ask,0,0,comment))
           { Print("Error opening a BUY position: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
        }
      //--- If there is a position
      else
        {
         //--- Get the position type
         pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         //--- If it is a SELL position
         if(pos_type==POSITION_TYPE_SELL)
           {
            //--- Get the position volume
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            //--- Adjust the volume
            lot=NormalizeDouble(pos_volume+start_lot,2);
            //--- Open a position. If the position failed to open, print the relevant message
            if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,lot,ask,0,0,comment))
              { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
           }
        }
      //---
      return;
     }
//--- If there is a Sell signal
   if(signal==1)
     {
      //-- Get the Bid price
      bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
      //--- If there is no position
      if(!pos_open)
        {
         //--- Open a position. If the position failed to open, print the relevant message
         if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,start_lot,bid,0,0,comment))
           { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
        }
      //--- If there is a position
      else
        {
         //--- Get the position type
         pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         //--- If it is a BUY position
         if(pos_type==POSITION_TYPE_BUY)
           {
            //--- Get the position volume
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            //--- Adjust the volume
            lot=NormalizeDouble(pos_volume+start_lot,2);
            //--- Open a position. If the position failed to open, print the relevant message
            if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,lot,bid,0,0,comment))
              { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
           }
        }
      //---
      return;
     }
  }

Creo que todo debería estar claro hasta el momento en el que se abre una posición. Como puede ver en el código de arriba, al puntero (trade) le sigue un punto, al que a su vez le sigue el método PositionOpen(). Así puede llamar a un método determinado de una clase. Tras poner un punto, verá una lista que contiene todos los métodos de clase. Todo lo que necesita es seleccionar el método requerido de la lista:

Fig. 1. Llamar a un método de clase.

Fig. 1. Llamar a un método de clase.

Hay dos bloques principales en la función TradingBlock() - para comprar y para vender. Tras determinar la dirección de la señal, obtendremos el precio ask en el caso de una señal de compra (Buy), y el precio bid en el caso de una señal de venta (Sell).

Todos los precios/niveles usados en órdenes de trading se debe normalizar usando la función NormalizeDouble(), de lo contrario cualquier intento de abrir o modificar una posición llevará a un error. El uso de esta función también es recomendable al calcular el lote. Además, tenga en cuenta que los parámetros Stop Loss y Take Profit tienen valores de cero. Se facilitará más información sobre la configuración de niveles de trading en el siguiente artículo de la serie.

Ahora que todas las funciones definidas por el usuario están listas, podemos colocarlas en el orden correcto:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize the new bar
   CheckNewBar();
//--- Get position properties and update the values on the panel
   GetPositionProperties();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Print the deinitialization reason to the journal
   Print(GetDeinitReasonText(reason));
//--- When deleting from the chart
   if(reason==REASON_REMOVE)
      //--- Delete all objects relating to the info panel from the chart
      DeleteInfoPanel();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- If the bar is not new, exit
   if(!CheckNewBar())
      return;
//--- If there is a new bar
   else
     {
      GetBarsData();  // Get bar data
      TradingBlock(); // Check the conditions and trade
     }
//--- Get the properties and update the values on the panel
   GetPositionProperties();
  }

Únicamente queda una cosa por considerar: cómo determinar eventos de trading usando la función OnTrade(). Solo trataremos este tema superficialmente para darle una idea general. En nuestro caso, debemos implementar el siguiente caso: al abrir/cerrar/modificar una posición manualmente, los valores en la lista de propiedades de posición en el panel de información deben actualizarse tan pronto como la operación se complete, en lugar de actualizarse a recibir un nuevo tick. Para ello, solo deberemos añadir el siguiente código:

//+------------------------------------------------------------------+
//| TRADE EVENT                                                      |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- Get position properties and update the values on the panel
   GetPositionProperties();
  }

Básicamente, todo está listo y podemos proceder a la simulación. El Probador de Estrategias le permitirá llevar a cabo una simulación en el modo de visualización y encontrar errores si los hay. El uso del Probador de Estrategias también resulta beneficioso, porque gracias a él puede seguir desarrollando su programa incluso durante los fines de semana, cuando los mercados están cerrados.

Configure el Probador de Estrategias, active el modo de visualización y haga click en Start. El Asesor Experto comenzará la simulación en el Probador de Estrategias y podrá ver una imagen parecida a la que se muestra abajo:

Fig. 2. Modo de visualización en el Probador de Estrategias de MetaTrader 5.

Fig. 2. Modo de visualización en el Probador de Estrategias de MetaTrader 5.

Puede suspender la simulación en el modo de visualización en cualquier momento y continuar la simulación paso a paso presionando la tecla F12. El paso será equivalente a una barra si se configura el Probador de Estrategias en el modo Opening prices only (solo precios de apertura), o a un tick si selecciona el modo Every tick (todos los ticks). También puede controlar la velocidad de simulación.

Para asegurarse de que los valores en el panel de información se actualizan después de abrir/cerrar una posición manualmente o añadir/modificar los niveles de Stop Loss/Take Profit, el Asesor Experto debe simularse en modo de tiempo real. Para no esperar demasiado tiempo, simplemente ejecute el Asesor Experto en un intervalo cronológico de un minuto para que las operaciones de trading se ejecuten cada minuto.

Además de eso, he añadido otro array para nombres de propiedades de posición en el panel de información:

// Array of position property names
string pos_prop_texts[INFOPANEL_SIZE]=
  {
   "Symbol :",
   "Magic Number :",
   "Comment :",
   "Swap :",
   "Commission :",
   "Open Price :",
   "Current Price :",
   "Profit :",
   "Volume :",
   "Stop Loss :",
   "Take Profit :",
   "Time :",
   "Identifier :",
   "Type :"
  };

En el artículo anterior mencioné que necesitaríamos este array para reducir el código de la función SetInfoPanel(). Ahora puede ver cómo se puede hacer esto si no lo ha implementado ya o no la había descubierto por sí mismo. La nueva implementación de la lista de creación de objetos relativa a propiedades de posición tiene el siguiente aspecto:

//--- List of the names of position properties and their values
   for(int i=0; i<INFOPANEL_SIZE; i++)
     {
      //--- Property name
      CreateLabel(0,0,pos_prop_names[i],pos_prop_texts[i],anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[i],2);
      //--- Property value
      CreateLabel(0,0,pos_prop_values[i],GetPropertyValue(i),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[i],2);
     }

Al principio de la función SetInfoPanel() puede ver la siguiente línea:

//--- Testing in the visualization mode
   if(MQL5InfoInteger(MQL5_VISUAL_MODE))
     {
      y_bg=2;
      y_property=16;
     }

Le comunica al programa que las coordenadas Y de los objetos en el panel de información debe ajustarse si el programa se está simulando actualmente en el modo de visualización. Esto se debe al hecho de que, al realizar una simulación en el modo de visualización del Probador de Estrategias, el nombre del Asesor Experto no se muestra en la esquina superior derecha del gráfico, tal y como se haría en tiempo real. Por tanto, se puede eliminar la sangría innecesaria.


Conclusión

Hemos terminado por ahora. En el siguiente artículo nos centraremos en configurar y modificar los niveles de trading. Abajo puede descargarse el código fuente del Asesor Experto, PositionPropertiesTesterEN.mq5.