English Русский 中文 Deutsch 日本語 Português
Experto comercial universal: Las estrategias de usuario y las clases comerciales auxiliares (Parte 3)

Experto comercial universal: Las estrategias de usuario y las clases comerciales auxiliares (Parte 3)

MetaTrader 5Ejemplos | 18 febrero 2016, 12:57
912 0
Vasiliy Sokolov
Vasiliy Sokolov

Contenido

 

Introducción

En esta parte de la serie de artículos continuaremos familiarizándonos con el motor comercial CStrategy. Recordemos brevemente el contenido de sus partes anteriores. En la primera parte, "Experto comercial universal: Los modos comerciales de las estrategias" se analizaron con detalle los modos comerciales, gracias a los que se puede configurar de forma flexible la lógica de funcionamiento de un experto, dependiendo de la hora y día de la semana. En el segundo artículo "Experto comercial universal: Modelo de eventos y prototipo de estrategia comercial" analizamos con detalle un modelo de eventos que toma como base el procesamiento centralizado de eventos, así como los algoritmos principales de la clase básica CStrategy, que constituyen los cimientos del experto de usuario.

En la tercera parte de la serie, describiremos con detalle ejemplos de asesores basados en el motor comercial ​​CStrategy, así como algunos algoritmos auxiliares que pueden ser necesarios para el desarrollo del asesor. Se prestará especial atención al procedimiento de registro. En realidad, el proceso de registro, a pesar de su función profundamente auxiliar, es un elemento muy importante en cualquier sistema más o menos complejo. Con la ayuda de un buen registrador, es posible comprender de forma rápida las causas de los fallos y encontrar el sitio donde ha tenido lugar dicho fallo. Este registrador se ha escrito usando una técnica de programación especial llamada patrón de "Single" (Singleton). Su análisis será interesante para aquellos que están interesados ​​no sólo en la organización del proceso comercial, sino también en la creación de algoritmos para resolver problemas no estándar.

Además, en este artículo se describen los algoritmos que permiten acceder a los datos de mercado a través de un índice cómodo e intuitivo. De hecho, para mucha gente, trabajar con datos a través de índices como Close[1] o High[0] es su peculiaridad de funcionamiento favorita en MetaTrader 4. Entonces, ¿por qué renunciar a ella, si se la puede utilizar exactamente igual en MetaTrader 5? Este artículo explica cómo hacerlo, y describe al detalle los algoritmos que implementan dicha posibilidad.

Me gustaría finalizar esta introducción con unas palabras del último artículo. El motor comercial CStrategy y sus algoritmos suponen un conjunto bastante complejo. Sin embargo, no se requerirá del usuario una comprensión completa de su funcionamiento. Bastará solamente con conocer los principios básicos y la lógica global del motor comercial. Por lo tanto, si alguna de las partes del artículo no está clara, podrá saltársela tranquilamente. Este es uno de los principios fundamentales del enfoque orientado a objetos: para utilizar bien un sistema complejo, no es obligatorio conocer su organización.

 

El registro de mensajes, las clases cMessage y CLog, el patrón "Solitario"

El registro es una de las tareas de apoyo más comunes. En general, en los programas pequeños se usa la función habitual Print o printf, que muestra un mensaje de error en el terminal MetaTrader 5:

...
double closes[];
if(CopyClose(Symbol(), Period(), 0, 100, closes) < 100)
   printf("Los datos son insuficientes.");
...

Sin embargo, este enfoque simple no siempre es suficiente para comprender lo que está sucediendo en los grandes programas que contienen muchos centenares de líneas de código fuente. Por ello, para este tipo de tareas es mejor escribir un módulo especial que se ocupará de las cuestiones relacionadas con el registro, la clase CLog.

El método más obvio de la clase de registro es el método AddMessage(). Por ejemplo, si Log es un objeto de nuestro registrador CLog, entonces se puede escribir la siguiente construcción:

Log.AddMessage("¡Atención! La cantidad de barras recibida es menor a la necesaria");

Sin embargo, la advertencia enviada contiene en realidad mucha menos información útil de la que necesitamos para la depuración. Por ejemplo, ¿cómo podemos saber por este mensaje cuándo se hizo? ¿Qué función específica lo ha creado? Por último, ¿cómo podemos entender la importancia de la información contenida en el mensaje? Para que esto no suceda, es necesario ampliar el propio concepto de mensaje. Cada mensaje, además del propio texto, debe contener los siguientes atributos:

  • la hora de creación;
  • la fuente del mensaje;
  • el tipo de mensaje (informativo, de advertencia, comunicación sobre un error).

También sería útil que nuestro mensaje, además de estos datos, contuviese información adicional:

  • el identificador del error de sistema;
  • el identificador del error comercial (si se ha realizado una acción comercial);
  • la hora del servidor comercial en el momento en el que se creó el mensaje.

Es más conveniente combinar todos estos datos en la clase especial CMessage. Gracias a que nuestro mensaje será una clase, en el futuro será fácil añadirle datos y métodos adicionales para trabajar con él. Bien, vamos a mostrar los encabezamientos de esta clase:

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>

#define UNKNOW_SOURCE "unknown"     // indicación sobre el origen indefinido de un mensaje
//+------------------------------------------------------------------+
//| Tipo de mensaje                                                  |
//+------------------------------------------------------------------+
enum ENUM_MESSAGE_TYPE
  {
   MESSAGE_INFO,                    // Mensaje informativo
   MESSAGE_WARNING,                 // Mensaje de advertencia
   MESSAGE_ERROR                    // Aparición de un error
  };
//+------------------------------------------------------------------+
//| Mensaje transmitido a la clase del registro                      |
//+------------------------------------------------------------------+
class CMessage : public CObject
  {
private:
   ENUM_MESSAGE_TYPE m_type;               // Tipo de mensaje
   string            m_source;             // Fuente del mensaje
   string            m_text;               // Texto del mensaje
   int               m_system_error_id;    // Contiene el identificador del error de SISTEMA
   int               m_retcode;            // Contiene el código de retorno del servidor comercial
   datetime          m_server_time;        // Hora del servidor comercial en el momento de la creación del mensaje
   datetime          m_local_time;         // Hora local en el momento de la creación del mensaje
   void              Init(ENUM_MESSAGE_TYPE type,string source,string text);
public:
                     CMessage(void);
                     CMessage(ENUM_MESSAGE_TYPE type);
                     CMessage(ENUM_MESSAGE_TYPE type,string source,string text);
   void              Type(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE Type(void);
   void              Source(string source);
   string            Source(void);
   void              Text(string text);
   string            Text(void);
   datetime          TimeServer(void);
   datetime          TimeLocal();
   void              SystemErrorID(int error);
   int               SystemErrorID();
   void              Retcode(int retcode);
   int               Retcode(void);
   string            ToConsoleType(void);
   string            ToCSVType(void);
  };

Lo primero que encontramos en el encabezamiento es la lista ENUM_MESSAGE_TYPE. Determina el tipo de mensaje a crear. El tipo de mensaje puede ser informativo (MESSAGE_INFO), de advertencia (MESSAGE_WARNING) o puede notificar la aparición de un error (MESSAGE_ERROR).

La clase en sí se compone de una variedad de métodos Get/Set que o bien establecen o bien calculan los diferentes atributos del mensaje. Para que sea más fácil crear mensajes en una sola línea, la clase cMessage dispone del constructor sobrecargado correspondiente, que debe ser llamado con parámetros que indiquen el texto del mensaje, el tipo y la fuente. Por ejemplo, si necesitamos crear en la función OnTick un mensaje que adivierta de que los datos cargados son insuficientes, se puede hacer de la forma siguiente:

void OnTick(void)
  {
   double closes[];
   if(CopyClose(Symbol(),Period(),0,100,closes)<100)
      CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Los datos son insuficientes");
  }

Este mensaje es mucho más informativo que el análogo anterior. Además del propio mensaje, este contiene el nombre de la función que lo invocó y el tipo del propio mensaje. Entre otras cosas, nuestro mensaje tiene datos que no hay necesidad de llenar en el momento de su creación. Por ejemplo, el objeto message contiene la hora de creación del mensaje y el código del error actual, si ha sucedido.

Ahora ha llegado el momento de analizar la clase de registro CLog. Esta clase realiza las funciones de repositorio de mensajes de CMessage. Una de sus características más interesantes también es el envío de notificaciones PUSH a los terminales móviles mediante el uso de la función SendNotification. Esta característica es muy útil cuando no existe la posibilidad de controlar constantemente el funcionamiento de los expertos. En lugar de ello, puede enviar a los dispositivos móviles del usuario mensajes sobre que algo ha ido mal.

Una peculiaridad del registro consiste en que este proceso debe ser único para todas las partes del programa. Sería extraño si cada función o clase tuviese su propio mecanismo de registro. Por eso la clase CLog se implementa mediante un patrón de programación especial, que se llama Solitario o Singleton. Este patrón asegura que un objeto de tipo determinado existe en un solo ejemplar. Por ejemplo, si el programa utiliza dos índices, cada uno de los cuales se refiere a un objeto del tipo CLog, entonces, en realidad, ambos índices harán referencia al mismo objeto. La creación y la eliminación real del objeto ocurre entre bastidores, en los métodos privados de la clase.

Veamos los encabezamientos de esta clase y los métodos que implementan el patrón "Solitario":

//+------------------------------------------------------------------+
//|                                                         Logs.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Message.mqh"

//+------------------------------------------------------------------+
//| Clase que organiza el registro de mensajes en forma de singleton |
//+------------------------------------------------------------------+
class CLog
{
private:
   static CLog*      m_log;                     // Índice al ejemplar estático global
   CArrayObj         m_messages;                // Lista de mensajes guardados
   bool              m_terminal_enable;         // Verdadero, si se necesita mostrar el mensaje recibido en el terminal comercial
   bool              m_push_enable;             // Si es verdadero, se enviarán notificaciones Push
   ENUM_MESSAGE_TYPE m_push_priority;           // Contiene la prioridad establecida paran la muestra de mensajes en la ventana del terminal
   ENUM_MESSAGE_TYPE m_terminal_priority;       // Contiene la prioridad establecida para el envío de mensajes a los dispositivos móviles de los destinatarios
   bool              m_recursive;               // Bandera que indica la llamada recursiva del destructor
   bool              SendPush(CMessage* msg);
   void              CheckMessage(CMessage* msg);
                     CLog(void);                // Constructor privado
   string            GetName(void);
   void              DeleteOldLogs(int day_history = 30);
   void              DeleteOldLog(string file_name, int day_history);
                     ~CLog(void){;}
public:
   static CLog*      GetLog(void);              // Método de obtención de un objeto estático
   bool              AddMessage(CMessage* msg);
   void              Clear(void);
   bool              Save(string path);
   CMessage*         MessageAt(int index)const;
   int               Total(void);
   void              TerminalEnable(bool enable);
   bool              TerminalEnable(void);
   void              PushEnable(bool enable);
   bool              PushEnable(void);
   void              PushPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE PushPriority(void);
   void              TerminalPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE TerminalPriority(void);
   bool              SaveToFile(void);
   static bool       DeleteLog(void);
};
CLog* CLog::m_log;

La clase CLog como miembro privado almacena un índice a un objeto estático de sí mismo. Puede parecer una construcción extraña en la programación, pero tiene sentido. El único constructor de clase es privado y no está disponible para la llamada. En lugar de llamar al constructor podemos utilizar el método Getlog:

//+------------------------------------------------------------------+
//| Retorna el objeto-registrador                                    |
//+------------------------------------------------------------------+
static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   return m_log;
}

Comprueba si el índice apunta a un objeto estático Clog existente, y si es así, devuelve un enlace al mismo. En caso contrario, crea un nuevo objeto y asocia con él al índice interno m_log. De esta forma, la creación de un objeto se produce sólo una vez. En las siguientes llamadas del método GetLog, se retornará el objeto creado anteriormente.

La eliminación del objeto también se produce sólo una vez. Para ello, se usa el método DeleteLog:

//+------------------------------------------------------------------+
//| Elimina el objeto-registrador                                    |
//+------------------------------------------------------------------+
bool CLog::DeleteLog(void)
{
   bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      delete m_log;
   return res;
}

El método, en el caso de que exista el objeto m_log, lo elimina y retorna verdadero.

Puede parecer que el sistema de registro descrito es bastante complejo, sin embargo, también las capacidades a las que da soporte son bastantes impresionantes. Por ejemplo, puede clasificar los mensajes por tipo, o enviar un mensaje como notificación PUSH. En última instancia, es el usuario quien debe decir si usar o no este sistema. Gracias a que está implementado en dos módulos por separado, Message.mqh y Logs.mqh, se lo puede usar tanto por separado del proyecto descrito, como de forma conjunta.

 

El acceso a las cotizaciones con la ayuda de los índices utilizados en MetaTrader 4

Uno de los mayores cambios en MetaTrader 5, en comparación con su predecesor, es el modelo de acceso a los datos y cotizaciones de los indicadores. Por ejemplo, en MetaTrader 4, para ver el precio de cierre de la barra actual, era suficiente escribir el siguiente procedimiento:

double close = Close[0];

Es decir, el acceso a los datos se realizaba prácticamente de forma directa, a través de la indexación de las series temporales correspondientes. En MetaTrader 5, para saber el precio de cierre de la barra actual, es necesario llevar a cabo más acciones:

  1. Definir la matriz-destinatario, a la que se copian las cotizaciones en la cantidad adecuada.
  2. Copiar las cotizaciones necesarias con la ayuda de una de las funciones del grupo Copy* (funciones para acceder a las series temporales y a los datos del indicador).
  3. Recurrir con el índice deseado de la matriz copiada.

Por ejemplo, para encontrar el precio de cierre de la barra actual en MetaTrader 5, debe seguir estos pasos:

double closes[];
double close = 0.0;
if(CopyClose(Symbol(), Period(), 0, 1, closes))
   close = closes[0];
else
   printf("No se pudo copiar el precio de cierre.");

Dicho acceso a los datos es más complicado que en MetaTrader 4. Sin embargo, precisamente gracias a ese acceso se logra la universalidad: se utiliza el mismo interfaz y mecanismos para el acceso a los datos obtenidos con diferentes símbolos e incluso indicadores.

Sin embargo, en las tareas cotidianas, por regla general, esto no se requiere. A menudo, sólo se requiere para obtener el último valor del instrumento actual. Este puede ser el precio de cierre o apertura de la barra, su mínimo o máximo. De una u otra forma, sería cómodo usar el modelo de acceso a los datos tomado de MetaTrader 4. Gracias al enfoque orientado a objetos del lenguaje MQL5, resulta posible la creación de clases especiales con indexadores que se pueden utilizar de la misma manera que es habitual en MetaTrader 4 para obtener acceso a los datos comerciales. Por ejemplo, para que en MetaTrader 5 se pueda obtener el precio de cierre de la barra actual de forma semejante:

double close = Close[0];

Es necesario escribir la siguiente clase-envoltorio para el precio de cierre:

//+------------------------------------------------------------------+
//| Acceso a los precios de cierre de la barra del instrumento.      |
//+------------------------------------------------------------------+
class CClose : public CSeries
  {
public:
   double operator[](int index)
     {
      double value[];
      if(CopyClose(m_symbol, m_timeframe, index, 1, value) == 0)return 0.0;
      return value[0];
     }
  };

El mismo código debe ser escrito para las otras series, incluyendo la hora, volumen, el precio de apertura, el máximo y el mínimo. Por supuesto, este código en algunos casos puede funcionar mucho más lentamente que el copiado de una vez de la matriz de cotizaciones necesaria con la ayuda de las funciones de sistema Copy*. Sin embargo, como se mencionó anteriormente, a menudo se necesita acceder únicamente al último elemento, dado que en la ventana deslizante todos los elementos anteriores ya han sido tenidos en cuenta.

Este sencillo conjunto de clases está incluido en el conjunto Series.mqh. Proporciona un interfaz cómodo para el acceso a las cotizaciones al estilo de MetaTrader 4 y se utiliza en el propio motor comercial.

Una característica distintiva de estas clases es su independencia de la plataforma. Por ejemplo, en MetaTrader 5 el experto comercial puede invocar al método de una de estas clases, "pensando" que se refiere a las cotizaciones directamente. Pero en MetaTrader 4, este método de acceso también funciona, solo que esta vez en lugar de utilizar una clase envoltorio especial, se realizará el acceso efectivo a las series de sistema del tipo Open, High, Low o Close.

 

El uso de indicadores orientados a objetos

Casi cada indicador tiene una serie de ajustes para su configuración. El funcionamiento con indicadores de MetaTrader 5 recuerda al trabajo con las cotizaciones, con la única diferencia de que antes de copiar los datos del indicador es necesario necesario crear el llamado manejador del indicador , un tipo especial de índice a un cierto objeto interno en MetaTrader 5, que contiene sus valores de cálculo. Los ajustes del indicador se establecen en el momento de la creación del manejador. Si por alguna razón es necesario cambiar uno de los parámetros del indicador, debe eliminar el antiguo manejador del indicador y crear uno nuevo, con los parámetros modificados. Los propios parámetros de este indicador deben ser guardados en algún lugar externo, por ejemplo, en las variables del experto.

Como resultado, la mayor parte del trabajo con los indicadores se transfiere al propio experto. Esto no siempre es cómodo. Consideremos un ejemplo sencillo: un experto comercia según las señales de cruce de una media móvil. A pesar de su sencillez, el indicador de media móvil tiene seis parámetros completos que debemos determinar:

  1. el símbolo del que se calcula la media móvil;
  2. el marco temporal o el periodo del gráfico;
  3. el periodo de promediación;
  4. el tipo de media móvil (simple, exponencial, ponderada, etc.);
  5. la desviación con respecto a la barra de precio;
  6. el precio utilizado (uno de los precios OHLC, o bien el búfer de cálculo de otro indicador).

Por lo tanto, si queremos escribir un experto sobre la intersección de dos medias móviles que usan la lista completa de los ajustes del indicador homónimo, entonces tendrá que contener doce parámetros: seis ajustes de media móvil rápida y seis para la lenta. Además, si el usuario cambia el marco temporal o un símbolo del gráfico en el que se ejecuta experto, los manejadores de indicadores utilizados también deberán reinicializarse.

Para liberar al experto de tareas relacionadas con el trabajo con los manejadores de los indicadores, debe utilizar la versión orientada a objetos de los mismos. Con la ayuda de estas clases-indicadores orientadas a objetos, se puede escribir una construcción parecida a esta:

CMovingAverageExp MAExpert;     // Creamos un experto que comercie conforme a dos medias móviles.
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Configuramos la media móvil rápida del experto
   MAExpert.FastMA.Symbol("EURUSD");
   MAExpert.FastMA.Symbol(PERIOD_M10);
   MAExpert.FastMA.Period(13);
   MAExpert.FastMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.FastMA.MaShift(1);
//--- Configuramos la media móvil lenta del experto
   MAExpert.SlowMA.Symbol("EURUSD");
   MAExpert.SlowMA.Symbol(PERIOD_M15);
   MAExpert.SlowMA.Period(15);
   MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.SlowMA.MaShift(1);

   return(INIT_SUCCEEDED);
  }

Al usuario final solo le queda establecer los propios parámetros de los indicadores usados por el experto. Al mismo tiempo, el propio experto sólo lee los datos desde ellos.

Hay otra ventaja importante del uso de los objetos-indicadores. Los indicadores orientados a objetos ocultan su implementación. Esto significa que pueden calcular sus valores tanto de forma independiente, como invocando los manejadores correspondientes. En el caso de que haya multitud de indicadores de cálculo y sea necesaria una alta velocidad de ejecución, es recomendable ubicar el bloque de cálculo del indicador directamente en el experto. Gracias al enfoque orientado a objetos, se puede hacer esto incluso sin reescribir el propio asesor experto. Para ello, basta con calcular los valores del indicador dentro de la clase correspondiente, sin recurrir al uso de manejadores.

Como ejemplo de un indicador de objeto semejante, vamos a mostrar el código fuente de la clase CIndMovingAverage, que, a su vez, está basado en el indicador de sistema iMA:

//+------------------------------------------------------------------+
//|                                                MovingAverage.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Message.mqh>
#include <Strategy\Logs.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
class CIndMovingAverage
  {
private:
   int               m_ma_handle;         // Manejador del indicador
   ENUM_TIMEFRAMES   m_timeframe;         // Marco temporal
   int               m_ma_period;         // Periodo
   int               m_ma_shift;          // Desplazamiento
   string            m_symbol;            // Instrumento
   ENUM_MA_METHOD    m_ma_method;         // Método de media móvil
   uint              m_applied_price;     // Manejador del indicador, en el se necesita calcular el valor de Moving Average,
                                          // o bien uno de los valores de precio ENUM_APPLIED_PRICE
   CLog*             m_log;               // Registro
   void              Init(void);
public:
                     CIndMovingAverage(void);

/*Params*/
   void              Timeframe(ENUM_TIMEFRAMES timeframe);
   void              MaPeriod(int ma_period);
   void              MaShift(int ma_shift);
   void              MaMethod(ENUM_MA_METHOD method);
   void              AppliedPrice(int source);
   void              Symbol(string symbol);

   ENUM_TIMEFRAMES   Timeframe(void);
   int               MaPeriod(void);
   int               MaShift(void);
   ENUM_MA_METHOD    MaMethod(void);
   uint              AppliedPrice(void);
   string            Symbol(void);

/*Out values*/
   double            OutValue(int index);
  };
//+------------------------------------------------------------------+
//| Constructor por defecto.                                         |
//+------------------------------------------------------------------+
CIndMovingAverage::CIndMovingAverage(void) : m_ma_handle(INVALID_HANDLE),
                                             m_timeframe(PERIOD_CURRENT),
                                             m_ma_period(12),
                                             m_ma_shift(0),
                                             m_ma_method(MODE_SMA),
                                             m_applied_price(PRICE_CLOSE)
  {
   m_log=CLog::GetLog();
  }
//+------------------------------------------------------------------+
//| Inicialización.                                                  |
//+------------------------------------------------------------------+
CIndMovingAverage::Init(void)
  {
   if(m_ma_handle!=INVALID_HANDLE)
     {
      bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         string text="Realise iMA indicator failed. Error ID: "+(string)GetLastError();
         CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text);
         m_log.AddMessage(msg);
        }
     }
   m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price);
   if(m_ma_handle==INVALID_HANDLE)
     {
      string params="(Period:"+(string)m_ma_period+", Shift: "+(string)m_ma_shift+
                    ", MA Method:"+EnumToString(m_ma_method)+")";
      string text="Create iMA indicator failed"+params+". Error ID: "+(string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
  }
//+------------------------------------------------------------------+
//| Establecer el marco temporal.                                    |
//+------------------------------------------------------------------+
void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Retorna el marco temporal actual.                                |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void)
  {
   return m_timeframe;
  }
//+------------------------------------------------------------------+
//| Establece el periodo de promediación de la media móvil.          |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaPeriod(int ma_period)
  {
   m_ma_period=ma_period;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Retorna el período actual de promediación de la media móvil.     |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaPeriod(void)
  {
   return m_ma_period;
  }
//+------------------------------------------------------------------+
//| Establece el tipo de media móvil.                                |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method)
  {
   m_ma_method=method;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Retorna el tipo de media móvil.                                  |
//+------------------------------------------------------------------+
ENUM_MA_METHOD CIndMovingAverage::MaMethod(void)
  {
   return m_ma_method;
  }
//+------------------------------------------------------------------+
//| Retorna el desplazamiento de la media móvil.                     |
//+------------------------------------------------------------------+
int CIndMovingAverage::MaShift(void)
  {
   return m_ma_shift;
  }
//+------------------------------------------------------------------+
//| Establece el desplazamiento de la media móvil.                   |
//+------------------------------------------------------------------+
void CIndMovingAverage::MaShift(int ma_shift)
  {
   m_ma_shift=ma_shift;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Establece el tipo de precio para el que se calcula la media.     |
//+------------------------------------------------------------------+
void CIndMovingAverage::AppliedPrice(int price)
  {
   m_applied_price = price;
   if(m_ma_handle != INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Retorna el tipo de precio para el que se calcula la media.       |
//+------------------------------------------------------------------+
uint CIndMovingAverage::AppliedPrice(void)
  {
   return m_applied_price;
  }
//+------------------------------------------------------------------+
//|Establece el símbolo en el que se debe calcular el indicador      |
//+------------------------------------------------------------------+
void CIndMovingAverage::Symbol(string symbol)
  {
   m_symbol=symbol;
   if(m_ma_handle!=INVALID_HANDLE)
      Init();
  }
//+------------------------------------------------------------------+
//| Retorna el símbolo para el que se calcula el indicador           |
//+------------------------------------------------------------------+
string CIndMovingAverage::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Retorna el valor de la media móvil según el índice index         |
//+------------------------------------------------------------------+
double CIndMovingAverage::OutValue(int index)
  {
   if(m_ma_handle==INVALID_HANDLE)
      Init();
   double values[];
   if(CopyBuffer(m_ma_handle,0,index,1,values))
      return values[0];
   return EMPTY_VALUE;
  }

Se trata de una clase bastante sencilla. Su tarea principal es reinicialización del indicador en caso de que cambie uno de los parámetros, así como el retorno del valor calculado según el índice index. El método Init reinicializa el manejador, y el método OutValue se encarga de retornar el valor necesario. Los métodos que retornan uno de los valores del indicador, comienzan con el prefijo Out. Esto facilita la búsqueda del método requerido al programar en los editores con sustitución intelectual de parámetros, por ejemplo, en el MetaEditor.

En el suministro del motor comercial entra una serie de indicadores orientados a objetos. Esto ayudará a entender su sencillo funcionamiento y le enseñará a crear sus propias versiones orientadas a objetos de indicadores clásicos. En el apartado dedicado a la escritura de expertos de usuario, se demuestra su funcionamiento de forma visual.

 

Los métodos a redefinir por parte del experto de usuario

En el primer artículo "Experto comercial universal: Los modos comerciales de las estrategias" examinamos con detalle los modos comerciales de la estrategia y sus métodos básicos que deben ser redefinidos. Ahora es el momento de pasar a la práctica.

Cada experto creado con la ayuda del motor comercial CStrategy debe redefinir los métodos virtuales responsables de algunas propiedades y del comportamiento del propio experto. Vamos a enumerar estos métodos redefinidos con un recuadro que consta de tres columnas. En la primera de ellas se muestra el nombre del método virtual, en la segunda, el evento o acción que se debe seguir o ejecutar. En la tercera se da una descripción del uso de este método. Aquí está el recuadro:

Método virtual Evento/acción Utilidad
OnSymbolChanged Se invoca cuando cambia el nombre del instrumento comercial En caso de cambiar el instrumento comercial, los indicadores del experto deben ser reinicializados. Este evento permite llevar a cabo una reinicialización de los indicadores del experto.
OnTimeframeChanged Cambio del marco temporal de trabajo En caso de cambiar el marco temporal de trabajo, los indicadores del experto deberán ser reinicializados. Este evento permite llevar a cabo una reinicialización de los indicadores del experto.
ParseXmlParams Análisis de los parámetros de usuario de la estrategia, descargados a través de un archivo XML La estrategia debe reconocer de forma autónoma los parámetros XML pasados ​​a este método, y configurar sus ajustes en consecuencia.
ExpertNameFull Retorna el nombre completo del experto El nombre completo del experto consta del nombre de estrategia y, por regla general, del conjunto único de parámetros de la estrategia misma. El ejemplar de la estrategia debe determinar el nombre completo para sí mismo de forma independiente. Este nombre también se utiliza en el panel visual, en la lista emergente Agent.
OnTradeTransaction Surge en el caso de que aparezca un evento comercial Para funcionar, algunas estrategias necesitan analizar los eventos comerciales. Este evento permite enviar el evento comercial al experto final, así como analizarlo.
InitBuy Inicializa la compra Uno de los métodos básicos que deben ser redefinidos. En este método, se debe realizar la compra, si se forman las condiciones comerciales adecuadas para ella.
InitSell Inicializa la venta Uno de los métodos básicos que deben ser redefinidos. En este método, es necesario realizar la venta, si se forman las condiciones comerciales adecuadas para ello.
SupportBuy Acompaña una posición larga abierta previamente La posición abierta larga anteriormente debe ser acompañada. Por ejemplo, ponerle una parada defensiva Stop Loss o cerrar la posición en caso de que aparezca una señal de salida de la posición. Todas estas acciones se deben ejecutar en este método.
SupportSell Acompaña una posición corta abierta con anterioridad La posición corta abierta anteriormente debe ser acompañada. Por ejemplo, ponerle una parada defensiva Stop Loss o cerrar la posición en caso de que aparezca una señal de salida de la posición. Todas estas acciones se deben ejecutar en este método.

 Recuadro 1. Los métodos virtuales y su designación

Los métodos más importantes a redefinir son InitBuy, InitSell, SupportBuy y SupportSell. En el recuadro están destacados en negrita. Si, por ejemplo, olvidamos redefinir el método InitBuy, la estrategia de usuario no realizará compras. Si no se redefine uno de los métodos Support, entonces la posición abierta podría no poderse cerrar nunca. Por lo tanto, al crear un experto, proceda a la redefinición de estos métodos con mucho cuidado.

Si usted quiere que el motor comercial cargue de fora automática la estrategia desde un archivo XML-y configure sus parámetros de acuerdo con los ajustes tomados desde este archivo, también será necesario que redefina el método ParseXmlParams. En este método, la estrategia por sí sola debería determinar los parámetros que se le transmiten, y entender cómo cambiar su propia configuración de acuerdo con estos parámetros. El trabajo con los parámetros XML será descrito con más detalle en la cuarta parte de la serie de artículos, llamada: "Experto comercial universal: El comercio en grupo y la gestión de la cartera de estrategias (Parte 4)". Un ejemplo de la redefinición del método ParseXmlParams se realiza en el listado de una estrategia basada en las bandas de Bollinger.

 

Ejemplo de un asesor que comercia con dos medias móviles

Es hora de crear nuestro primer asesor experto, que usará las capacidades de CStrategy. Para hacer que el código fuente sea más simple y más compacto, no vamos a utilizar el registro. Describiremos brevemente las acciones que debe llevar a cabo en nuestro asesor:

  • Al cambiar el marco temporal y el símbolo, se cambiarán los ajustes de los indicadores de las medias móviles rápida y lenta, para lo cual se redefinen los métodos OnSymbolChanged y OnTimeframeChanged.
  • Redefinir los métodos InitBuy, InitSell, SupportBuy y SupportSell. Ubicar en estos métodos la lógica comercial del experto (reglas de apertura y acompañamiento de las posiciones).

El resto del trabajo lo deberá llevar a cabo por el experto su motor comercial y los indicadores utilizados por él. Aquí está el código fuente del experto:

//+------------------------------------------------------------------+
//|                                                      Samples.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
#include <Strategy\Indicators\MovingAverage.mqh>
//+------------------------------------------------------------------+
//| Ejemplo de una estrategia clásica de dos medias móviles.         |
//| Si la media móvil rápida cruza la lenta de abajo hacia arriba    |
//| entonces compramos, si la cruza de arriba hacia abajo, vendemos. |
//+------------------------------------------------------------------+
class CMovingAverage : public CStrategy
  {
private:
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
public:
   CIndMovingAverage FastMA;        // Media móvil rápida
   CIndMovingAverage SlowMA;        // Media móvil lenta
                     CMovingAverage(void);
   virtual string    ExpertNameFull(void);
  };
//+------------------------------------------------------------------+
//| Inicialización.                                                  |
//+------------------------------------------------------------------+
CMovingAverage::CMovingAverage(void)
  {
  }
//+------------------------------------------------------------------+
//| Reaccionamos al cambio de símbolo                                |
//+------------------------------------------------------------------+
void CMovingAverage::OnSymbolChanged(string new_symbol)
  {
   FastMA.Symbol(new_symbol);
   SlowMA.Symbol(new_symbol);
  }
//+------------------------------------------------------------------+
//| Reaccionamos al cambio de marco temporal                         |
//+------------------------------------------------------------------+
void CMovingAverage::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   FastMA.Timeframe(new_tf);
   SlowMA.Timeframe(new_tf);
  }
//+------------------------------------------------------------------+
//| Compramos cuando la media móvil rápida esté por encima           |
//| de la lenta.                                                     |
//+------------------------------------------------------------------+
void CMovingAverage::InitBuy(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // ¡Procesamos solo el vento necesario!
   if(positions.open_buy > 0) return;                    // ¡Si hay aunque sea solo una posición larga, no hay que comprar más, puesto que ya hemos comprado!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Si no hay posición de compra, vemos que ahora la media móvil rápida está por encima de la lenta:
      Trade.Buy(MM.GetLotFixed(), ExpertSymbol(), "");   // Si está por encima, compramos.
  }
//+------------------------------------------------------------------+
//| Cerramos la posición larga, cuando la media móvil rápida         |
//| está por debajo de la lenta.                                     |
//+------------------------------------------------------------------+
void CMovingAverage::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // ¡Procesamos solo el vento necesario!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Si la media rápida está por debajo de la lenta -   
      pos.CloseAtMarket("Exit by cross over");           // Cerramos la posición.
  }
//+------------------------------------------------------------------+
//| Compramos cuando la media móvil rápida esté por encima           |
//| de la lenta.                                                     |
//+------------------------------------------------------------------+
void CMovingAverage::InitSell(const MarketEvent &event)
  {
   if(!IsTrackEvents(event))return;                      // ¡Procesamos solo el vento necesario!
   if(positions.open_sell > 0) return;                   // ¡Si hay aunque sea solo una posición corta, no hay que vender más, puesto que ya hemos vendido!
   if(FastMA.OutValue(1) < SlowMA.OutValue(1))           // Si no hay posición de venta, vemos que ahora la media móvil rápida está por encima de la lenta:
      Trade.Sell(1.0, ExpertSymbol(), "");               // Si está por encima, compramos.
  }
//+------------------------------------------------------------------+
//| Cerramos la posición corta cuando la media móvil rápida          |
//| esté por encima de la lenta.                                     |
//+------------------------------------------------------------------+
void CMovingAverage::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(!IsTrackEvents(event))return;                      // ¡Procesamos solo el vento necesario!
   if(FastMA.OutValue(1) > SlowMA.OutValue(1))           // Si la media móvil rápida está por encima de la lenta -    
      pos.CloseAtMarket("Exit by cross under");          // Cerramos la posición.
  }
//+------------------------------------------------------------------+
//| Cierra los eventos entrantes. Si el evento transmitido           |
//| no es procesado por la estrategia, retorna falso, si se procesa- |
//| retorna verdadero.                                               |
//+------------------------------------------------------------------+
bool CMovingAverage::IsTrackEvents(const MarketEvent &event)
  {
//--- Procesamos solo la apertura de una nueva barra en el instrumento y marco temporal en el que trabajamos
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }

El código es bastante simple de entender. Sin embargo, será necesario aclarar algunos puntos. El motor comercial CStrategy inicia los métodos InitBuy, InitSell, SupportBuy y SuportSell (métodos de lógica comercial) con la aparición de cualquier evento, por ejemplo, al cambiar la profundidad de mercado, con la llegada de un nuevo tick o con el cambio del temporizador. Por lo general, estos métodos son invocados con mucha frecuencia. Sin embargo, el experto utiliza un conjunto muy limitado de eventos. Concretamente, este experto usa solo un evento, la formación de una nueva barra. Por eso los otros eventos que invocan los métodos de la lógica comercial se deben ignorar. Para este propósito precisamente sirve método IsTrackEvents. Este comprueba si el evento que se le transfiere es rastreable, y si es así, devuelve verdadero, de lo contrario devuelve falso.

Como variable auxiliar se usa la estructura positions. Contiene la cantidad de posiciones largas y cortas que pertenecen a la estrategia actual. El motor CStrategy calcula de forma independiente estas estadísticas, la estrategia en sí no tiene que pasar por todas las posiciones abiertas para contar su número. De esta forma, toda la lógica del experto con respecto a la apertura de posición se reduce a la comprobación de las siguientes condiciones:

  1. si el evento comercial es la apertura de un nueva barra;
  2. no hay más posiciones abiertas en la dirección establecida;
  3. la media móvil rápida está por encima (para la compra) o por debajo (para la venta) de la media móvil lenta.

Las condiciones que se deben cumplir para cerrar la posición son aún más fáciles:

  1. si el evento comercial es la apertura de un nueva barra;
  2. la media móvil rápida está por debajo (para el cierre de una posición larga) o por encima (para el cierre de una posición corta) de la media móvil lenta.

Al mismo tiempo, comprobar las posiciones abiertas no es necesario en absoluto, porque el hecho de que se llame a SupportBuy y SupportSell con la posición actual en calidad de parámetro, indica que existe una posición del experto y que ha sido transmitida al mismo para su gestión.

Notemos que la ropia lógica del experto, sin tener en cuenta la definición de los métodos y su clase, se describe en solo 18 líneas de código. Y además, la mitad de estas líneas (las condiciones de venta) son el reverso de la otra mitad (las condiciones de compra). Esta simplificación de la lógica sólo es posible cuando se utilizan las bibliotecas auxiliares, lo que precisamente constituye el motor comercial CStrategy.

 

Ejemplo de un asesor basado en la ruptura del canal BollingerBands

Vamos a continuar la creación de estrategias que utilizan el motor comercial CStrategy. En el segundo ejemplo vamos a crear una estrategia que comercie con la ruptura del canal de Bollinger. Si el precio actual es mayor que el canal superior de Bollinger, realizamos la compra. Y al contrario, si el precio actual de cierre de la barra está por debajo del borde inferior de Bollinger, realizamos la venta. La salida de las posiciones larga y corta la realizaremos al alcanzar el precio de la línea media de este indicador.

Esta vez vamos a utilizar el manejador estándar del indicador iBands. Esto se realiza con el propósito de demostrar que, en el marco de nuestro modelo de comercio es posible también trabajar con los manejadores de los inndicadores directamente, es decir, no hay necesidad de construir clases especiales orientadas a objetos de los indicadores. Sin embargo, en este caso, deberemos ubicar en el experto los dos parámetros principales del indicador: su período de promediación y la magnitud de la desviación estándar. Aquí tenemos el código fuente de esta estrategia:

//+------------------------------------------------------------------+
//|                                                ChannelSample.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#include <Strategy\Strategy.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
class CChannel : public CStrategy
  {
private:
   int               m_handle;   // Manejador del indicador que vamos a utilizar
   int               m_period;   // Periodo de Bollinger
   double            m_std_dev;  // Magnitud de la desviación estándar
   bool              IsTrackEvents(const MarketEvent &event);
protected:
   virtual void      OnSymbolChanged(string new_symbol);
   virtual void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
   virtual void      InitBuy(const MarketEvent &event);
   virtual void      SupportBuy(const MarketEvent &event,CPosition *pos);
   virtual void      InitSell(const MarketEvent &event);
   virtual void      SupportSell(const MarketEvent &event,CPosition *pos);
   virtual bool      ParseXmlParams(CXmlElement *params);
   virtual string    ExpertNameFull(void);
public:
                     CChannel(void);
                    ~CChannel(void);
   int               PeriodBands(void);
   void              PeriodBands(int period);
   double            StdDev(void);
   void              StdDev(double std);
  };
//+------------------------------------------------------------------+
//| Constructor por defecto                                          |
//+------------------------------------------------------------------+
CChannel::CChannel(void) : m_handle(INVALID_HANDLE)
  {
  }
//+------------------------------------------------------------------+
//| El destructor libera el manejador utilizado del indicador        |
//+------------------------------------------------------------------+
CChannel::~CChannel(void)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
  }
//+------------------------------------------------------------------+
//| Reaccionamos al cambio de símbolo                                |
//+------------------------------------------------------------------+
void CChannel::OnSymbolChanged(string new_symbol)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(new_symbol,Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Reaccionamos al cambio de marco temporal                         |
//+------------------------------------------------------------------+
void CChannel::OnTimeframeChanged(ENUM_TIMEFRAMES new_tf)
  {
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Retorna el periodo del indicador                                 |
//+------------------------------------------------------------------+
int CChannel::PeriodBands(void)
  {
   return m_period;
  }
//+------------------------------------------------------------------+
//| Establece el periodo del indicador                               |
//+------------------------------------------------------------------+
void CChannel::PeriodBands(int period)
  {
   if(m_period == period)return;
   m_period=period;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Establece la magnitud de la desviación estándar                  |
//+------------------------------------------------------------------+
double CChannel::StdDev(void)
  {
   return m_std_dev;
  }
//+------------------------------------------------------------------+
//| Establece la magnitud de la desviación estándar                  |
//+------------------------------------------------------------------+
void CChannel::StdDev(double std)
  {
   if(m_std_dev == std)return;
   m_std_dev=std;
   if(m_handle!=INVALID_HANDLE)
      IndicatorRelease(m_handle);
   m_handle=iBands(ExpertSymbol(),Timeframe(),m_period,0,m_std_dev,PRICE_CLOSE);
  }
//+------------------------------------------------------------------+
//| Reglas de apertura de una posición larga                         |
//+------------------------------------------------------------------+
void CChannel::InitBuy(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Activamos la lógica solo en la apertura de una nueva barra
   if(positions.open_buy > 0)return;                  // No abre más de una posición larga
   double bands[];
   if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]>bands[0])
      Trade.Buy(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| Reglas de cierre de la posición larga                            |
//+------------------------------------------------------------------+
void CChannel::SupportBuy(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;                    // Activamos la lógica solo en la apertura de una nueva barra
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]<bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| Reglas de apertura de una posición larga                         |
//+------------------------------------------------------------------+
void CChannel::InitSell(const MarketEvent &event)
  {
   if(IsTrackEvents(event))return;                    // Activamos la lógica solo en la apertura de una nueva barra
   if(positions.open_sell > 0)return;                 // No abre más de una posición larga
   double bands[];
   if(CopyBuffer(m_handle, LOWER_BAND, 1, 1, bands) == 0)return;
   if(Close[1]<bands[0])
      Trade.Sell(1.0,ExpertSymbol());
  }
//+------------------------------------------------------------------+
//| Reglas de cierre de la posición larga                            |
//+------------------------------------------------------------------+
void CChannel::SupportSell(const MarketEvent &event,CPosition *pos)
  {
   if(IsTrackEvents(event))return;     // Activamos la lógica solo en la apertura de una nueva barra
   double bands[];
   if(CopyBuffer(m_handle, BASE_LINE, 1, 1, bands) == 0)return;
   double b = bands[0];
   double s = Close[1];
   if(Close[1]>bands[0])
      pos.CloseAtMarket();
  }
//+------------------------------------------------------------------+
//| Cierra los eventos entrantes. Si el evento transmitido           |
//| no es procesado por la estrategia, retorna falso, si se procesa- |
//| retorna verdadero.                                               |
//+------------------------------------------------------------------+
bool CChannel::IsTrackEvents(const MarketEvent &event)
  {
//--- Procesamos solo la apertura de una nueva barra en el instrumento y marco temporal en el que trabajamos
   if(event.type != MARKET_EVENT_BAR_OPEN)return false;
   if(event.period != Timeframe())return false;
   if(event.symbol != ExpertSymbol())return false;
   return true;
  }
//+------------------------------------------------------------------+
//| Los parámetros específicos para la estrategia se analizan        |
//| en ella misma en el método dado, redefinido desde CStrategy      |
//+------------------------------------------------------------------+
bool CChannel::ParseXmlParams(CXmlElement *params)
  {
   bool res=true;
   for(int i=0; i<params.GetChildCount(); i++)
     {
      CXmlElement *param=params.GetChild(i);
      string name=param.GetName();
      if(name=="Period")
         PeriodBands((int)param.GetText());
      else if(name=="StdDev")
         StdDev(StringToDouble(param.GetText()));
      else
         res=false;
     }
   return res;
  }
//+------------------------------------------------------------------+
//| Nombre completo y único del experto                              |
//+------------------------------------------------------------------+
string CChannel::ExpertNameFull(void)
  {
   string name=ExpertName();
   name += "[" + ExpertSymbol();
   name += "-" + StringSubstr(EnumToString(Timeframe()), 7);
   name += "-" + (string)Period();
   name += "-" + DoubleToString(StdDev(), 1);
   name += "]";
   return name;
  }

Ahora el trabajo que realiza el asesor es más amplio. El asesor contiene los parámetros de promediación de Bollinger y la magnitud de su desviación. Asimismo, el experto se ocupa de la creación de los manejadores de los indicadores y de su eliminación en los métodos correspondientes. Este es el precio por usar los indicadores de sistema directamente, sin clases-envoltorio especiales. En todos lo demás, su código es análogo al del experto anterior. Espera a que el precio de cierre de la última barra esté por encima (para la compra) o por debajo (para la venta) de las bandas de Bollinger, y abre una nueva posición.

Preste atención a que en este asesor se usa el acceso directo a las barras a través de las clases especiales de la serie temporal. Por ejemplo, así se compara el último precio de cierre conocido de la barra con la franja superior de Bollinger en la sección de compras (método InitBuy):

double bands[];
if(CopyBuffer(m_handle, UPPER_BAND, 1, 1, bands) == 0)return;
if(Close[1] > bands[0])
   Trade.Buy(1.0, ExpertSymbol());

Aparte de los métodos ya conocidos, el experto contiene los métodos redefinidos ExpertNameFull y ParseXmlParams. El primero de estos métodos determina el nombre único del experto, se refleja en el panel de usuario y en calidad de nombre del experto. El segundo método descarga los ajustes del indicador de Bollinger desde un archivo XML. Sobre el panel de usuario y los ajustes del experto guardados en archivos XML hablaremos en el cuarto y último artículo de la serie. En lo demás, el funcionamiento del experto es semejante al asesor anterior. Esto es lo que se propone el enfoque propuesto: la unificación completa de la escritura de un asesor.

 

Cargando las estrategias de usuario en el motor comercial

Después de que todas las estrategias hayan sido descritas, es necesario crear sus ejemplares, analizar con los parámetros necesarios y añadirlos al motor comercial. Cualquier estrategia que se carga en el motor comercial debe tener ciertos atributos requeridos (propiedades instauradas) que debería retornar. Estos atributos incluyen las siguientes propiedades:

  • El identificador único de la estrategia (su número mágico). Los números de las estrategias deben ser únicos, incluso si se crean como ejemplares de una clase. Para indicar el número único es necesario usar el método-Set ExpertMagic() de la estrategia.
  • El marco temporal de la estrategia (o su periodo de funcionamiento). Si la estrategia funciona en varios periodos de trabajo simultáneamente, habrá que indicar el marco temporal de trabajo de todas formas. En este caso, puede ser, por ejemplo, el plazo de tiempo más comúnmente utilizado. Para indicar el período de trabajo, se debe utilizar el método-Set Timeframe.
  • El símbolo de la estrategia (o su instrumento de trabajo). Si la estrategia funciona con varios instrumentos (estrategia multi-instrumental), seguirá siendo necesario especificar el símbolo de trabajo. Este puede ser uno de los símbolos que utiliza la estrategia.
  • El nombre de la estrategia. Cada estrategia está obligada, aparte de tener estos atributos, a disponer de su propio nombre en forma de línea string. El nombre del experto se llena usando el método-Set ExpertName. Esta propiedad es necesaria ya que precisamente esta se utiliza para la creación automática de las estrategias del archivo Strategies.xml. Asimismo, esta propiedad se utiliza para mostrar la estrategia del panel de usuario, cuya descripción se da en la cuarta parte de la serie de artículos.

Si aunque sea uno solo de estos atributos no se rellena, el motor comercial se negará a cargar el algoritmo, remitiendo un mensaje de advertencia sobre el parámetro que falta.

El motor comercial en sí consta de dos partes principales:

  • El módulo externo de gestión de estrategias CStrategyList. Este módulo es el gestor de las estrategias y contiene los algoritmos para gestionarlas. Hablaremos de forma más detallada sobre este módulo en la siguiente parte de esta serie artículos.
  • El módulo interno de estrategias CStrategy. Este módulo define el funcional básico de la estrategia. Ha sido descrito con detalle en este artículo y el anterior: "Experto comercial universal: Modelo de eventos y prototipo de estrategia comercial (Parte 2)".

Cada ejemplar de la estrategia CStrategy debe ser cargado en el gestor de estrategias CStrategyList. El gestor de estrategias permite cargar estrategias de dos maneras:

  • De forma automática, con la ayuda del archivo de configuración Strategies.xml. Por ejemplo, es posible describir un conjunto de estrategias con sus parámetros en este archivo, y al ejecutar el experto en el gráfico, el gestor de estrategias creará él mismo los ejemplares necesarios de las estrategias, inicializará sus parámetros y los añadirá a su lista. Este método se describe con detalle en el siguiente artículo.
  • De forma manual, con la ayuda de la descripción directa en el módulo ejecutor. Al usar este enfoque, en la sección OnInit del asesor experto, con la ayuda de un conjunto de instrucciones se crea el objeto-estrategia correspondiente, después se inicializa con los parámetros necesarios y se añade al gestor de estrategias CStrategyList.

A continuación describimos cómo realizar la configuración manualmente. Para ello, crearemos el archivo Agent.mq5 con los siguientes contenidos:

//+------------------------------------------------------------------+
//|                                                        Agent.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Strategy\StrategiesList.mqh>
#include <Strategy\Samples\ChannelSample.mqh>
#include <Strategy\Samples\MovingAverage.mqh>
CStrategyList Manager;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Configuramos y añadimos a la lista de estrategias CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
   ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;

//--- Configuramos y añadimos a la lista de estrategias CChannel
   CChannel *channel=new CChannel();
   channel.ExpertMagic(1216);
   channel.Timeframe(Period());
   channel.ExpertSymbol(Symbol());
   channel.ExpertName("Bollinger Bands");
   channel.PeriodBands(50);
   channel.StdDev(2.0);
   if(!Manager.AddStrategy(channel))
      delete channel;

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   Manager.OnTick();
  }
//+------------------------------------------------------------------+
//| BookEvent function                                               |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
   Manager.OnBookEvent(symbol);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Manager.OnChartEvent(id,lparam,dparam,sparam);
  }

En el listing mostrado, se puede ver que la configuración de estrategias se realiza en la función OnInit. Si se olvida de especificar uno de los parámetros necesarios de la estrategia, el gestor de estrategias se negará a añadir esta estrategia a su lista. En esta caso, el método AddStartegy retornará false y el ejemplar de estrategias creado deberá ser eliminado. El mensaje de alerta emitido por el gestor de estrategias ayudará a entender el problema potencial. Vamos a tratar de llamar a un mensaje de este tipo. Para ello, comentamos las instrucciones que establecen el número mágico de la estrategia:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Configuramos y añadimos a la lista de estrategias CMovingAverage
   CMovingAverage *ma=new CMovingAverage();
 //ma.ExpertMagic(1215);
   ma.Timeframe(Period());
   ma.ExpertSymbol(Symbol());
   ma.ExpertName("Moving Average");
   ma.FastMA.MaPeriod(10);
   ma.SlowMA.MaPeriod(23);
   if(!Manager.AddStrategy(ma))
      delete ma;
   return(INIT_SUCCEEDED);
  }

Después de iniciar el módulo ejecutable, en el terminal se mostrará el mensaje siguiente:

2016.01.20 14:08:54.995 AgentWrong (FORTS-USDRUB,H1)    WARNING;CStrategyList::AddStrategy;The strategy should have a magic number. Adding strategy Moving Average is impossible;2016.01.20 14:09:01

A partir del mensaje, queda claro que el método CStrategyList::AddStartegy no ha podido añadir la estrategia, puesto que su número mágico no ha sido establecido.

Aparte de la configuración de las estrategias, el archivo Agent.mq5 incluye dentro el procesamiento de los eventos comerciales que deben ser analizados. El procesamiento consiste en realizar un seguimiento de los eventos y transmitirlos a los métodos correspondientes de la clase CStrategyList.

Una vez creado el archivo ejecutable, puede ser compilado. Los códigos fuente de las estrategias analizadas se encuentran en el catálogo Include\Strategy\Samples y están disponibles en el archivo adjunto. El experto compilado estará listo para usar e incluirá la lógica de dos estrategias comerciales a la vez.

 

Conclusión

Hemos estudiado ejemplos de escritura de estrategias de usuario y el principio de funcionamiento de las clases que proporcionan acceso a las cotizaciones a través de indexadores simples. Asimismo, hemos estudiado las clases que organizan el registro y ejemplos de indicadores orientados a objetos. Gracias al concepto propuesto de construcción de un asesor experto, resulta bastante sencillo formalizar la lógica del sistema comercial. Todo lo que necesitamos es describir las normas en varios métodos redefinidos.

En la cuarta parte de esta serie de artículos, llamada: "Experto comercial universal: El comercio en grupo y la gestión de la cartera de estrategias (Parte 4)" describiremos los algoritmos con ayuda de los cuales se puede añadir una cantidad ilimitada de lógicas comerciales en un módulo ejecutable del experto ex5. Asimismo, en la cuarta parte se propone el análisis de un sencillo panel de usuario, con el que se pueden gestionar los expertos que se encuentren en el interior del módulo ejecutable, por ejemplo cambiar su modo comercial o realizar en su nombre acciones comerciales de compra y venta.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/2170

Archivos adjuntos |
strategyarticle.zip (100.23 KB)
Experto comercial universal: El comercio en grupo y la gestión de la cartera de estrategias (Parte 4) Experto comercial universal: El comercio en grupo y la gestión de la cartera de estrategias (Parte 4)
En la parte definitiva de esta serie de artículos sobre el motor comercial CStrategy, estudiaremos el funcionamiento simultáneo de múltiples algoritmos comerciales, la descarga de estrategias desde archivos XML, así como la presentación de un sencillo panel para la selección de expertos, que se encuentra dentro de un módulo ejecutable único, y veremos la gestión de los modos comerciales de los mismos.
Interfaces gráficas I: Funciones para los botones del formulario y eliminación de los elementos de la interfaz (Capítulo 4) Interfaces gráficas I: Funciones para los botones del formulario y eliminación de los elementos de la interfaz (Capítulo 4)
En el presente artículo vamos a continuar desarrollando la clase CWindow. La clase será ampliada con los métodos que permitirán gestionar el formulario haciendo clics en sus controles. Vamos a implementar la posibilidad de cerrar el programa usando el botón en el formulario, así como minimizar y maximizar el formulario en caso de necesidad.
Interfaces gráficas I: Probamos la librería en los programas de diferentes tipos y en el terminal MetaTrader 4 (Capítulo 5) Interfaces gráficas I: Probamos la librería en los programas de diferentes tipos y en el terminal MetaTrader 4 (Capítulo 5)
En el capítulo anterior de la primera parte de la serie sobre las interfaces gráficas, en la clase del formulario han sido añadidos los métodos que permiten manejar el formulario con los clics en los controles. En el presente artículo vamos a testear el trabajo realizado en diferentes tipos de programas MQL, como indicadores y scripts. En vista de que se ha planteado la tarea de diseñar una librería multiplataforma (en marco de las plataformas comerciales MetaTrader), también realizaremos las pruebas en MetaTrader 4.
Gestión de errores y logging en MQL5 Gestión de errores y logging en MQL5
Este artículo se centra en aspectos generales sobre el manejo de los errores de software. Explicaremos qué significa el término logging mediante algunos ejemplos implementados con herramientas de MQL5.